public inbox for [email protected]help / color / mirror / Atom feed
Re: Acceptance Tests against a browser (WIP) 30+ messages / 3 participants [nested] [flat]
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-17 16:09 Atira Odhner <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Atira Odhner @ 2017-01-17 16:09 UTC (permalink / raw) To: pgadmin-hackers; +Cc: George Gelashvili <[email protected]> Thanks for your feedback, Dave! We can put the tests under the regression directory. I think that makes sense. I'm not picturing these tests being module specific, but we may want to enable running it as a separate suite of tests. Thanks for the callout about the port and title. We'll make sure those are pulled from config or that the pgAdmin server is spun up by the test with specific values. I have a couple ideas about why the test might not have been running for you. I think the patch we attached didn't spin up its own pgAdmin yet and it definitely doesn't fill in username/password if your app is running that way. That's part of the WIP-ness :-P -Tira Hi On Thu, Jan 12, 2017 at 10:41 PM, George Gelashvili <ggelashvili(at)pivotal(dot)io> wrote: > here's the patch we forgot to attach. Also, you can see work on our branch > at: > https://github.com/pivotalsoftware/pgadmin4/tree/pivotal/acceptance-tests > > On Thu, Jan 12, 2017 at 5:26 PM, George Gelashvili <ggelashvili(at)pivotal(dot)io> > wrote: >> >> Hi there, >> >> We are working on browser-automation-based acceptance tests that exercise >> pgAdmin4 the way a user might. Nice! >> The first "connect to database" test works, but at the moment depends on >> Chrome and chromedriver. We would appreciate feedback on any possible >> license or code style issues at this point, as well as any thoughts on >> adding this sort of test to the codebase. A few thoughts: - If these tests are to run as part of the regression suite, the framework for them should live under that directory. - Are any of the tests likely to be module-specific? If so, they should really be part of the relevant module as the regression tests are. If they're more general/less tightly coupled, then I don't see a problem with them residing where they are. - Please take care not to include changes to .gitgnore files that aren't relevant to the rest of us. - The port number is hard-coded in the test. - You've hard-coded the string "pgAdmin 4". We've tried to keep that title as a config option in config.py, so you should pull the string from there rather than hard-coding it. - The connect test fails for me (Mac, Python 2.7). I have a suspicion that this may be because when the test starts chromedriver, OS X prompts the user about whether a listening port should be opened, but the tests don't wait (though, I tested with 3 servers configured and it failed with the same error on the second and third as well, long after I clicked OK on the prompt): Traceback (most recent call last): File "/Users/dpage/git/pgadmin4/web/acceptance/test_connects_to_database.py", line 32, in runTest self.assertEqual("pgAdmin 4", self.driver.title) AssertionError: 'pgAdmin 4' != u'localhost' - Please keep tests in the pgadmin. namespace (pgadmin.acceptance.??). - It looks like running a single test won't work yet (because of TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % arguments['pkg'])) Thanks! -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-19 22:07 George Gelashvili <[email protected]> parent: Atira Odhner <[email protected]> 0 siblings, 2 replies; 30+ messages in thread From: George Gelashvili @ 2017-01-19 22:07 UTC (permalink / raw) To: Atira Odhner <[email protected]>; +Cc: pgadmin-hackers Here is an updated patch which starts the server up when the test starts and uses the values from config.py for server name etc. It still requires installing chromedriver before running. Should we add something to the readme about that? On Tue, Jan 17, 2017 at 11:09 AM, Atira Odhner <[email protected]> wrote: > Thanks for your feedback, Dave! > > We can put the tests under the regression directory. I think that makes > sense. > I'm not picturing these tests being module specific, but we may want to > enable running it as a separate suite of tests. > > Thanks for the callout about the port and title. We'll make sure those are > pulled from config or that the pgAdmin server is spun up by the test with > specific values. > > I have a couple ideas about why the test might not have been running for > you. I think the patch we attached didn't spin up its own pgAdmin yet and > it definitely doesn't fill in username/password if your app is running that > way. That's part of the WIP-ness :-P > > -Tira > > Hi > > On Thu, Jan 12, 2017 at 10:41 PM, George Gelashvili > <ggelashvili(at)pivotal(dot)io> wrote: > > here's the patch we forgot to attach. Also, you can see work on our branch > > at: > > https://github.com/pivotalsoftware/pgadmin4/tree/pivotal/acceptance-tests > > > > On Thu, Jan 12, 2017 at 5:26 PM, George Gelashvili <ggelashvili(at)pivotal(dot)io> > > wrote: > >> > >> Hi there, > >> > >> We are working on browser-automation-based acceptance tests that exercise > >> pgAdmin4 the way a user might. > > Nice! > > >> The first "connect to database" test works, but at the moment depends on > >> Chrome and chromedriver. We would appreciate feedback on any possible > >> license or code style issues at this point, as well as any thoughts on > >> adding this sort of test to the codebase. > > A few thoughts: > > - If these tests are to run as part of the regression suite, the > framework for them should live under that directory. > > - Are any of the tests likely to be module-specific? If so, they > should really be part of the relevant module as the regression tests > are. If they're more general/less tightly coupled, then I don't see a > problem with them residing where they are. > > - Please take care not to include changes to .gitgnore files that > aren't relevant to the rest of us. > > - The port number is hard-coded in the test. > > - You've hard-coded the string "pgAdmin 4". We've tried to keep that > title as a config option in config.py, so you should pull the string > from there rather than hard-coding it. > > - The connect test fails for me (Mac, Python 2.7). I have a suspicion > that this may be because when the test starts chromedriver, OS X > prompts the user about whether a listening port should be opened, but > the tests don't wait (though, I tested with 3 servers configured and > it failed with the same error on the second and third as well, long > after I clicked OK on the prompt): > > Traceback (most recent call last): > File "/Users/dpage/git/pgadmin4/web/acceptance/test_connects_to_database.py", > line 32, in runTest > self.assertEqual("pgAdmin 4", self.driver.title) > AssertionError: 'pgAdmin 4' != u'localhost' > > - Please keep tests in the pgadmin. namespace (pgadmin.acceptance.??). > > - It looks like running a single test won't work yet (because of > TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % > arguments['pkg'])) > > Thanks! > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > > > diff --git a/requirements_py2.txt b/requirements_py2.txt index 51170a45..de167121 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index f68db7a8..9565a6e4 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/test_connects_to_database.py b/web/pgadmin/acceptance/tests/test_connects_to_database.py new file mode 100644 index 00000000..ccbcf9f5 --- /dev/null +++ b/web/pgadmin/acceptance/tests/test_connects_to_database.py @@ -0,0 +1,108 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config + + +class ConnectsToDatabase(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if (config.SERVER_MODE): + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid) + + print("waiting for server to start") + time.sleep(10) + + print("opening browser") + self.driver = webdriver.Chrome() + self.server_config = self.server + print(config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT)) + self.driver.get("http://"; + config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT)) + + def runTest(self): + self.assertEqual(config.APP_NAME, self.driver.title) + self._wait_for_spinner_to_disappear() + + self._find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self._find_by_partial_link_text("Server...").click() + + self._fill_input_by_xpath("name", self.server_config['name']) + self._find_by_partial_link_text("Connection").click() + self._fill_input_by_xpath("host", self.server_config['host']) + self._fill_input_by_xpath("port", self.server_config['port']) + self._fill_input_by_xpath("username", self.server_config['username']) + self._fill_input_by_xpath("password", self.server_config['db_password']) + self._find_by_xpath("//button[contains(.,'Save')]").click() + + self._find_by_xpath("//*[@id='tree']//*[.='" + self.server_config['name'] + "']") + + def tearDown(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) + + def failureException(self, *args, **kwargs): + self.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) + + def _find_by_xpath(self, xpath): + return self._wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def _find_by_partial_link_text(self, link_text): + return self._wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def _fill_input_by_xpath(self, field_name, field_content): + self._find_by_xpath("//input[@name='" + field_name + "']").clear() + self._find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def _wait_for_element(self, find_method_with_args): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + time_waited += sleep_time + time.sleep(sleep_time) + pass + + self.fail("Timed out waiting for element") + + def _wait_for_spinner_to_disappear(self): + try: + while True: + self.driver.find_element_by_id("pg-spinner") + time.sleep(0.1) + except NoSuchElementException: + pass diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..fed26a0f 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, *pkgs): cls.registry = dict() + all_modules = [] + + for pkg in pkgs: + all_modules += find_modules(pkg, False, True) + + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): + for module_name in all_modules: + if config.SERVER_MODE: try: if "tests." in str(module_name): import_module(module_name) except ImportError: traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): + else: try: # Exclude the test cases in browser node if SERVER_MODE # is False diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [text/plain] acceptance-tests-with-server-start.diff (6.6K, 3-acceptance-tests-with-server-start.diff) download | inline diff: diff --git a/requirements_py2.txt b/requirements_py2.txt index 51170a45..de167121 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index f68db7a8..9565a6e4 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/test_connects_to_database.py b/web/pgadmin/acceptance/tests/test_connects_to_database.py new file mode 100644 index 00000000..ccbcf9f5 --- /dev/null +++ b/web/pgadmin/acceptance/tests/test_connects_to_database.py @@ -0,0 +1,108 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config + + +class ConnectsToDatabase(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if (config.SERVER_MODE): + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid) + + print("waiting for server to start") + time.sleep(10) + + print("opening browser") + self.driver = webdriver.Chrome() + self.server_config = self.server + print(config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT)) + self.driver.get("http://" + config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT)) + + def runTest(self): + self.assertEqual(config.APP_NAME, self.driver.title) + self._wait_for_spinner_to_disappear() + + self._find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self._find_by_partial_link_text("Server...").click() + + self._fill_input_by_xpath("name", self.server_config['name']) + self._find_by_partial_link_text("Connection").click() + self._fill_input_by_xpath("host", self.server_config['host']) + self._fill_input_by_xpath("port", self.server_config['port']) + self._fill_input_by_xpath("username", self.server_config['username']) + self._fill_input_by_xpath("password", self.server_config['db_password']) + self._find_by_xpath("//button[contains(.,'Save')]").click() + + self._find_by_xpath("//*[@id='tree']//*[.='" + self.server_config['name'] + "']") + + def tearDown(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) + + def failureException(self, *args, **kwargs): + self.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) + + def _find_by_xpath(self, xpath): + return self._wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def _find_by_partial_link_text(self, link_text): + return self._wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def _fill_input_by_xpath(self, field_name, field_content): + self._find_by_xpath("//input[@name='" + field_name + "']").clear() + self._find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def _wait_for_element(self, find_method_with_args): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + time_waited += sleep_time + time.sleep(sleep_time) + pass + + self.fail("Timed out waiting for element") + + def _wait_for_spinner_to_disappear(self): + try: + while True: + self.driver.find_element_by_id("pg-spinner") + time.sleep(0.1) + except NoSuchElementException: + pass diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..fed26a0f 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, *pkgs): cls.registry = dict() + all_modules = [] + + for pkg in pkgs: + all_modules += find_modules(pkg, False, True) + + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): + for module_name in all_modules: + if config.SERVER_MODE: try: if "tests." in str(module_name): import_module(module_name) except ImportError: traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): + else: try: # Exclude the test cases in browser node if SERVER_MODE # is False diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-19 23:15 George Gelashvili <[email protected]> parent: George Gelashvili <[email protected]> 1 sibling, 1 reply; 30+ messages in thread From: George Gelashvili @ 2017-01-19 23:15 UTC (permalink / raw) To: Atira Odhner <[email protected]>; +Cc: pgadmin-hackers Here's an updated patch which polls to wait for the app to start instead of sleeping for 10 seconds. On Thu, Jan 19, 2017 at 5:07 PM, George Gelashvili <[email protected]> wrote: > > Here is an updated patch which starts the server up when the test starts > and uses the values from config.py for server name etc. It still requires > installing chromedriver before running. Should we add something to the > readme about that? > > On Tue, Jan 17, 2017 at 11:09 AM, Atira Odhner <[email protected]> wrote: > >> Thanks for your feedback, Dave! >> >> We can put the tests under the regression directory. I think that makes >> sense. >> I'm not picturing these tests being module specific, but we may want to >> enable running it as a separate suite of tests. >> >> Thanks for the callout about the port and title. We'll make sure those >> are pulled from config or that the pgAdmin server is spun up by the test >> with specific values. >> >> I have a couple ideas about why the test might not have been running for >> you. I think the patch we attached didn't spin up its own pgAdmin yet and >> it definitely doesn't fill in username/password if your app is running that >> way. That's part of the WIP-ness :-P >> >> -Tira >> >> Hi >> >> On Thu, Jan 12, 2017 at 10:41 PM, George Gelashvili >> <ggelashvili(at)pivotal(dot)io> wrote: >> > here's the patch we forgot to attach. Also, you can see work on our branch >> > at: >> > https://github.com/pivotalsoftware/pgadmin4/tree/pivotal/acceptance-tests >> > >> > On Thu, Jan 12, 2017 at 5:26 PM, George Gelashvili <ggelashvili(at)pivotal(dot)io> >> > wrote: >> >> >> >> Hi there, >> >> >> >> We are working on browser-automation-based acceptance tests that exercise >> >> pgAdmin4 the way a user might. >> >> Nice! >> >> >> The first "connect to database" test works, but at the moment depends on >> >> Chrome and chromedriver. We would appreciate feedback on any possible >> >> license or code style issues at this point, as well as any thoughts on >> >> adding this sort of test to the codebase. >> >> A few thoughts: >> >> - If these tests are to run as part of the regression suite, the >> framework for them should live under that directory. >> >> - Are any of the tests likely to be module-specific? If so, they >> should really be part of the relevant module as the regression tests >> are. If they're more general/less tightly coupled, then I don't see a >> problem with them residing where they are. >> >> - Please take care not to include changes to .gitgnore files that >> aren't relevant to the rest of us. >> >> - The port number is hard-coded in the test. >> >> - You've hard-coded the string "pgAdmin 4". We've tried to keep that >> title as a config option in config.py, so you should pull the string >> from there rather than hard-coding it. >> >> - The connect test fails for me (Mac, Python 2.7). I have a suspicion >> that this may be because when the test starts chromedriver, OS X >> prompts the user about whether a listening port should be opened, but >> the tests don't wait (though, I tested with 3 servers configured and >> it failed with the same error on the second and third as well, long >> after I clicked OK on the prompt): >> >> Traceback (most recent call last): >> File "/Users/dpage/git/pgadmin4/web/acceptance/test_connects_to_database.py", >> line 32, in runTest >> self.assertEqual("pgAdmin 4", self.driver.title) >> AssertionError: 'pgAdmin 4' != u'localhost' >> >> - Please keep tests in the pgadmin. namespace (pgadmin.acceptance.??). >> >> - It looks like running a single test won't work yet (because of >> TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % >> arguments['pkg'])) >> >> Thanks! >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> >> >> > diff --git a/requirements_py2.txt b/requirements_py2.txt index 51170a45..de167121 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index f68db7a8..9565a6e4 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/test_connects_to_database.py b/web/pgadmin/acceptance/tests/test_connects_to_database.py new file mode 100644 index 00000000..2c4f85c4 --- /dev/null +++ b/web/pgadmin/acceptance/tests/test_connects_to_database.py @@ -0,0 +1,123 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config + + +class ConnectsToDatabase(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if (config.SERVER_MODE): + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid, stderr=open(os.devnull, 'w')) + + self.driver = webdriver.Chrome() + self.server_config = self.server + + print("opening browser") + self.driver.get("http://"; + config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT)) + self._wait_for_app() + + def runTest(self): + self.assertEqual(config.APP_NAME, self.driver.title) + self._wait_for_spinner_to_disappear() + + self._find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self._find_by_partial_link_text("Server...").click() + + self._fill_input_by_xpath("name", self.server_config['name']) + self._find_by_partial_link_text("Connection").click() + self._fill_input_by_xpath("host", self.server_config['host']) + self._fill_input_by_xpath("port", self.server_config['port']) + self._fill_input_by_xpath("username", self.server_config['username']) + self._fill_input_by_xpath("password", self.server_config['db_password']) + self._find_by_xpath("//button[contains(.,'Save')]").click() + + self._find_by_xpath("//*[@id='tree']//*[.='" + self.server_config['name'] + "']") + + def tearDown(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) + + def failureException(self, *args, **kwargs): + self.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) + + def _find_by_xpath(self, xpath): + return self._wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def _find_by_partial_link_text(self, link_text): + return self._wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def _fill_input_by_xpath(self, field_name, field_content): + self._find_by_xpath("//input[@name='" + field_name + "']").clear() + self._find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def _wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self.__wait_for("element to exist", element_if_it_exists) + + def _wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self.__wait_for("spinner to disappear", spinner_has_disappeared) + + def _wait_for_app(self): + def page_shows_app(): + self.driver.refresh() + return self.driver.title == config.APP_NAME + + self.__wait_for("app to start", page_shows_app) + + def __wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if(result): + return result + time_waited += sleep_time + time.sleep(sleep_time) + + self.fail("Timed out waiting for " + waiting_for_message) diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..fed26a0f 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, *pkgs): cls.registry = dict() + all_modules = [] + + for pkg in pkgs: + all_modules += find_modules(pkg, False, True) + + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): + for module_name in all_modules: + if config.SERVER_MODE: try: if "tests." in str(module_name): import_module(module_name) except ImportError: traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): + else: try: # Exclude the test cases in browser node if SERVER_MODE # is False diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [text/plain] acceptance-tests-with-server-start-and-polling.diff (7.1K, 3-acceptance-tests-with-server-start-and-polling.diff) download | inline diff: diff --git a/requirements_py2.txt b/requirements_py2.txt index 51170a45..de167121 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index f68db7a8..9565a6e4 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/test_connects_to_database.py b/web/pgadmin/acceptance/tests/test_connects_to_database.py new file mode 100644 index 00000000..2c4f85c4 --- /dev/null +++ b/web/pgadmin/acceptance/tests/test_connects_to_database.py @@ -0,0 +1,123 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config + + +class ConnectsToDatabase(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if (config.SERVER_MODE): + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid, stderr=open(os.devnull, 'w')) + + self.driver = webdriver.Chrome() + self.server_config = self.server + + print("opening browser") + self.driver.get("http://" + config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT)) + self._wait_for_app() + + def runTest(self): + self.assertEqual(config.APP_NAME, self.driver.title) + self._wait_for_spinner_to_disappear() + + self._find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self._find_by_partial_link_text("Server...").click() + + self._fill_input_by_xpath("name", self.server_config['name']) + self._find_by_partial_link_text("Connection").click() + self._fill_input_by_xpath("host", self.server_config['host']) + self._fill_input_by_xpath("port", self.server_config['port']) + self._fill_input_by_xpath("username", self.server_config['username']) + self._fill_input_by_xpath("password", self.server_config['db_password']) + self._find_by_xpath("//button[contains(.,'Save')]").click() + + self._find_by_xpath("//*[@id='tree']//*[.='" + self.server_config['name'] + "']") + + def tearDown(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) + + def failureException(self, *args, **kwargs): + self.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) + + def _find_by_xpath(self, xpath): + return self._wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def _find_by_partial_link_text(self, link_text): + return self._wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def _fill_input_by_xpath(self, field_name, field_content): + self._find_by_xpath("//input[@name='" + field_name + "']").clear() + self._find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def _wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self.__wait_for("element to exist", element_if_it_exists) + + def _wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self.__wait_for("spinner to disappear", spinner_has_disappeared) + + def _wait_for_app(self): + def page_shows_app(): + self.driver.refresh() + return self.driver.title == config.APP_NAME + + self.__wait_for("app to start", page_shows_app) + + def __wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if(result): + return result + time_waited += sleep_time + time.sleep(sleep_time) + + self.fail("Timed out waiting for " + waiting_for_message) diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..fed26a0f 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, *pkgs): cls.registry = dict() + all_modules = [] + + for pkg in pkgs: + all_modules += find_modules(pkg, False, True) + + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): + for module_name in all_modules: + if config.SERVER_MODE: try: if "tests." in str(module_name): import_module(module_name) except ImportError: traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): + else: try: # Exclude the test cases in browser node if SERVER_MODE # is False diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-20 15:38 Dave Page <[email protected]> parent: George Gelashvili <[email protected]> 0 siblings, 0 replies; 30+ messages in thread From: Dave Page @ 2017-01-20 15:38 UTC (permalink / raw) To: George Gelashvili <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers Hi On Thu, Jan 19, 2017 at 11:15 PM, George Gelashvili <[email protected]> wrote: > Here's an updated patch which polls to wait for the app to start instead of > sleeping for 10 seconds. I see the browser opening now, and it immediately loads pgAdmin. Then, it prompts me for a reload whenever the test is run, which fails no matter how quickly I hit the prompt from what I can see: ====================================================================== ERROR: runTest (pgadmin.acceptance.tests.test_connects_to_database.ConnectsToDatabase) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/test_connects_to_database.py", line 41, in setUp self._wait_for_app() File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/test_connects_to_database.py", line 109, in _wait_for_app self.__wait_for("app to start", page_shows_app) File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/test_connects_to_database.py", line 117, in __wait_for result = condition_met_function() File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/test_connects_to_database.py", line 107, in page_shows_app return self.driver.title == config.APP_NAME File "/Users/dpage/.virtualenvs/pgadmin4/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py", line 257, in title resp = self.execute(Command.GET_TITLE) File "/Users/dpage/.virtualenvs/pgadmin4/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py", line 236, in execute self.error_handler.check_response(response) File "/Users/dpage/.virtualenvs/pgadmin4/lib/python2.7/site-packages/selenium/webdriver/remote/errorhandler.py", line 192, in check_response raise exception_class(message, screen, stacktrace) UnexpectedAlertPresentException: Alert Text: None Message: unexpected alert open: {Alert text : Are you sure you wish to close the pgAdmin 4 browser?} (Session info: chrome=55.0.2883.95) (Driver info: chromedriver=2.27.440174 (e97a722caafc2d3a8b807ee115bfb307f7d2cfd9),platform=Mac OS X 10.12.1 x86_64) -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-20 15:38 Dave Page <[email protected]> parent: George Gelashvili <[email protected]> 1 sibling, 1 reply; 30+ messages in thread From: Dave Page @ 2017-01-20 15:38 UTC (permalink / raw) To: George Gelashvili <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers On Thu, Jan 19, 2017 at 10:07 PM, George Gelashvili <[email protected]> wrote: > > Here is an updated patch which starts the server up when the test starts and > uses the values from config.py for server name etc. It still requires > installing chromedriver before running. Should we add something to the > readme about that? Yes, we definitely should (including download site URL) > On Tue, Jan 17, 2017 at 11:09 AM, Atira Odhner <[email protected]> wrote: >> >> Thanks for your feedback, Dave! >> >> We can put the tests under the regression directory. I think that makes >> sense. >> I'm not picturing these tests being module specific, but we may want to >> enable running it as a separate suite of tests. >> >> Thanks for the callout about the port and title. We'll make sure those are >> pulled from config or that the pgAdmin server is spun up by the test with >> specific values. >> >> I have a couple ideas about why the test might not have been running for >> you. I think the patch we attached didn't spin up its own pgAdmin yet and it >> definitely doesn't fill in username/password if your app is running that >> way. That's part of the WIP-ness :-P >> >> -Tira >> >> Hi >> >> On Thu, Jan 12, 2017 at 10:41 PM, George Gelashvili >> <ggelashvili(at)pivotal(dot)io> wrote: >> > here's the patch we forgot to attach. Also, you can see work on our >> > branch >> > at: >> > >> > https://github.com/pivotalsoftware/pgadmin4/tree/pivotal/acceptance-tests >> > >> > On Thu, Jan 12, 2017 at 5:26 PM, George Gelashvili >> > <ggelashvili(at)pivotal(dot)io> >> > wrote: >> >> >> >> Hi there, >> >> >> >> We are working on browser-automation-based acceptance tests that >> >> exercise >> >> pgAdmin4 the way a user might. >> >> Nice! >> >> >> The first "connect to database" test works, but at the moment depends >> >> on >> >> Chrome and chromedriver. We would appreciate feedback on any possible >> >> license or code style issues at this point, as well as any thoughts on >> >> adding this sort of test to the codebase. >> >> A few thoughts: >> >> - If these tests are to run as part of the regression suite, the >> framework for them should live under that directory. >> >> - Are any of the tests likely to be module-specific? If so, they >> should really be part of the relevant module as the regression tests >> are. If they're more general/less tightly coupled, then I don't see a >> problem with them residing where they are. >> >> - Please take care not to include changes to .gitgnore files that >> aren't relevant to the rest of us. >> >> - The port number is hard-coded in the test. >> >> - You've hard-coded the string "pgAdmin 4". We've tried to keep that >> title as a config option in config.py, so you should pull the string >> from there rather than hard-coding it. >> >> - The connect test fails for me (Mac, Python 2.7). I have a suspicion >> that this may be because when the test starts chromedriver, OS X >> prompts the user about whether a listening port should be opened, but >> the tests don't wait (though, I tested with 3 servers configured and >> it failed with the same error on the second and third as well, long >> after I clicked OK on the prompt): >> >> Traceback (most recent call last): >> File >> "/Users/dpage/git/pgadmin4/web/acceptance/test_connects_to_database.py", >> line 32, in runTest >> self.assertEqual("pgAdmin 4", self.driver.title) >> AssertionError: 'pgAdmin 4' != u'localhost' >> >> - Please keep tests in the pgadmin. namespace (pgadmin.acceptance.??). >> >> - It looks like running a single test won't work yet (because of >> TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % >> arguments['pkg'])) >> >> Thanks! >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> >> > > > > -- > Sent via pgadmin-hackers mailing list ([email protected]) > To make changes to your subscription: > http://www.postgresql.org/mailpref/pgadmin-hackers > -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-20 17:33 George Gelashvili <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: George Gelashvili @ 2017-01-20 17:33 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers Thanks for bringing that to our attention! Here's the latest patch On Fri, Jan 20, 2017 at 10:38 AM, Dave Page <[email protected]> wrote: > On Thu, Jan 19, 2017 at 10:07 PM, George Gelashvili > <[email protected]> wrote: > > > > Here is an updated patch which starts the server up when the test starts > and > > uses the values from config.py for server name etc. It still requires > > installing chromedriver before running. Should we add something to the > > readme about that? > > Yes, we definitely should (including download site URL) > > > On Tue, Jan 17, 2017 at 11:09 AM, Atira Odhner <[email protected]> > wrote: > >> > >> Thanks for your feedback, Dave! > >> > >> We can put the tests under the regression directory. I think that makes > >> sense. > >> I'm not picturing these tests being module specific, but we may want to > >> enable running it as a separate suite of tests. > >> > >> Thanks for the callout about the port and title. We'll make sure those > are > >> pulled from config or that the pgAdmin server is spun up by the test > with > >> specific values. > >> > >> I have a couple ideas about why the test might not have been running for > >> you. I think the patch we attached didn't spin up its own pgAdmin yet > and it > >> definitely doesn't fill in username/password if your app is running that > >> way. That's part of the WIP-ness :-P > >> > >> -Tira > >> > >> Hi > >> > >> On Thu, Jan 12, 2017 at 10:41 PM, George Gelashvili > >> <ggelashvili(at)pivotal(dot)io> wrote: > >> > here's the patch we forgot to attach. Also, you can see work on our > >> > branch > >> > at: > >> > > >> > https://github.com/pivotalsoftware/pgadmin4/tree/ > pivotal/acceptance-tests > >> > > >> > On Thu, Jan 12, 2017 at 5:26 PM, George Gelashvili > >> > <ggelashvili(at)pivotal(dot)io> > >> > wrote: > >> >> > >> >> Hi there, > >> >> > >> >> We are working on browser-automation-based acceptance tests that > >> >> exercise > >> >> pgAdmin4 the way a user might. > >> > >> Nice! > >> > >> >> The first "connect to database" test works, but at the moment depends > >> >> on > >> >> Chrome and chromedriver. We would appreciate feedback on any possible > >> >> license or code style issues at this point, as well as any thoughts > on > >> >> adding this sort of test to the codebase. > >> > >> A few thoughts: > >> > >> - If these tests are to run as part of the regression suite, the > >> framework for them should live under that directory. > >> > >> - Are any of the tests likely to be module-specific? If so, they > >> should really be part of the relevant module as the regression tests > >> are. If they're more general/less tightly coupled, then I don't see a > >> problem with them residing where they are. > >> > >> - Please take care not to include changes to .gitgnore files that > >> aren't relevant to the rest of us. > >> > >> - The port number is hard-coded in the test. > >> > >> - You've hard-coded the string "pgAdmin 4". We've tried to keep that > >> title as a config option in config.py, so you should pull the string > >> from there rather than hard-coding it. > >> > >> - The connect test fails for me (Mac, Python 2.7). I have a suspicion > >> that this may be because when the test starts chromedriver, OS X > >> prompts the user about whether a listening port should be opened, but > >> the tests don't wait (though, I tested with 3 servers configured and > >> it failed with the same error on the second and third as well, long > >> after I clicked OK on the prompt): > >> > >> Traceback (most recent call last): > >> File > >> "/Users/dpage/git/pgadmin4/web/acceptance/test_connects_ > to_database.py", > >> line 32, in runTest > >> self.assertEqual("pgAdmin 4", self.driver.title) > >> AssertionError: 'pgAdmin 4' != u'localhost' > >> > >> - Please keep tests in the pgadmin. namespace (pgadmin.acceptance.??). > >> > >> - It looks like running a single test won't work yet (because of > >> TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % > >> arguments['pkg'])) > >> > >> Thanks! > >> > >> -- > >> Dave Page > >> Blog: http://pgsnake.blogspot.com > >> Twitter: @pgsnake > >> > >> EnterpriseDB UK: http://www.enterprisedb.com > >> The Enterprise PostgreSQL Company > >> > >> > > > > > > > > -- > > Sent via pgadmin-hackers mailing list ([email protected]) > > To make changes to your subscription: > > http://www.postgresql.org/mailpref/pgadmin-hackers > > > > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > diff --git a/requirements_py2.txt b/requirements_py2.txt index 51170a45..de167121 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index f68db7a8..9565a6e4 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/test_connects_to_database.py b/web/pgadmin/acceptance/tests/test_connects_to_database.py new file mode 100644 index 00000000..c35cd0fd --- /dev/null +++ b/web/pgadmin/acceptance/tests/test_connects_to_database.py @@ -0,0 +1,127 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config + + +class ConnectsToDatabase(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + self.driver = webdriver.Chrome() + self.server_config = self.server + + print("opening browser") + self.driver.get("http://"; + config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT)) + self._wait_for_app() + + def runTest(self): + self.assertEqual(config.APP_NAME, self.driver.title) + self._wait_for_spinner_to_disappear() + + self._find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self._find_by_partial_link_text("Server...").click() + + self._fill_input_by_xpath("name", self.server_config['name']) + self._find_by_partial_link_text("Connection").click() + self._fill_input_by_xpath("host", self.server_config['host']) + self._fill_input_by_xpath("port", self.server_config['port']) + self._fill_input_by_xpath("username", self.server_config['username']) + self._fill_input_by_xpath("password", self.server_config['db_password']) + self._find_by_xpath("//button[contains(.,'Save')]").click() + + self._find_by_xpath("//*[@id='tree']//*[.='" + self.server_config['name'] + "']") + + def tearDown(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) + + def failureException(self, *args, **kwargs): + self.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) + + def _find_by_xpath(self, xpath): + return self._wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def _find_by_partial_link_text(self, link_text): + return self._wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def _fill_input_by_xpath(self, field_name, field_content): + self._find_by_xpath("//input[@name='" + field_name + "']").clear() + self._find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def _wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self.__wait_for("element to exist", element_if_it_exists) + + def _wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self.__wait_for("spinner to disappear", spinner_has_disappeared) + + def _wait_for_app(self): + def page_shows_app(): + if self.driver.title == config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self.__wait_for("app to start", page_shows_app) + + def __wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + self.fail("Timed out waiting for " + waiting_for_message) diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..fed26a0f 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, *pkgs): cls.registry = dict() + all_modules = [] + + for pkg in pkgs: + all_modules += find_modules(pkg, False, True) + + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): + for module_name in all_modules: + if config.SERVER_MODE: try: if "tests." in str(module_name): import_module(module_name) except ImportError: traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): + else: try: # Exclude the test cases in browser node if SERVER_MODE # is False diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..ae5d268d 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index a2d2f5cd..1f9f0522 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -69,7 +69,7 @@ def get_config_data(): """This function reads the server data from config_data""" server_data = [] for srv in test_setup.config_data['server_credentials']: - if (not srv.has_key('enabled')) or srv['enabled'] == True: + if (not 'enabled' in srv) or srv['enabled']: data = {"name": srv['name'], "comment": srv['comment'], "host": srv['host'], -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [text/plain] acceptance-tests-with-server-start-and-polling.diff (8.5K, 3-acceptance-tests-with-server-start-and-polling.diff) download | inline diff: diff --git a/requirements_py2.txt b/requirements_py2.txt index 51170a45..de167121 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index f68db7a8..9565a6e4 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/test_connects_to_database.py b/web/pgadmin/acceptance/tests/test_connects_to_database.py new file mode 100644 index 00000000..c35cd0fd --- /dev/null +++ b/web/pgadmin/acceptance/tests/test_connects_to_database.py @@ -0,0 +1,127 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config + + +class ConnectsToDatabase(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + self.driver = webdriver.Chrome() + self.server_config = self.server + + print("opening browser") + self.driver.get("http://" + config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT)) + self._wait_for_app() + + def runTest(self): + self.assertEqual(config.APP_NAME, self.driver.title) + self._wait_for_spinner_to_disappear() + + self._find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self._find_by_partial_link_text("Server...").click() + + self._fill_input_by_xpath("name", self.server_config['name']) + self._find_by_partial_link_text("Connection").click() + self._fill_input_by_xpath("host", self.server_config['host']) + self._fill_input_by_xpath("port", self.server_config['port']) + self._fill_input_by_xpath("username", self.server_config['username']) + self._fill_input_by_xpath("password", self.server_config['db_password']) + self._find_by_xpath("//button[contains(.,'Save')]").click() + + self._find_by_xpath("//*[@id='tree']//*[.='" + self.server_config['name'] + "']") + + def tearDown(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) + + def failureException(self, *args, **kwargs): + self.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) + + def _find_by_xpath(self, xpath): + return self._wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def _find_by_partial_link_text(self, link_text): + return self._wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def _fill_input_by_xpath(self, field_name, field_content): + self._find_by_xpath("//input[@name='" + field_name + "']").clear() + self._find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def _wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self.__wait_for("element to exist", element_if_it_exists) + + def _wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self.__wait_for("spinner to disappear", spinner_has_disappeared) + + def _wait_for_app(self): + def page_shows_app(): + if self.driver.title == config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self.__wait_for("app to start", page_shows_app) + + def __wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + self.fail("Timed out waiting for " + waiting_for_message) diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..fed26a0f 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, *pkgs): cls.registry = dict() + all_modules = [] + + for pkg in pkgs: + all_modules += find_modules(pkg, False, True) + + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): + for module_name in all_modules: + if config.SERVER_MODE: try: if "tests." in str(module_name): import_module(module_name) except ImportError: traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): + else: try: # Exclude the test cases in browser node if SERVER_MODE # is False diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..ae5d268d 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index a2d2f5cd..1f9f0522 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -69,7 +69,7 @@ def get_config_data(): """This function reads the server data from config_data""" server_data = [] for srv in test_setup.config_data['server_credentials']: - if (not srv.has_key('enabled')) or srv['enabled'] == True: + if (not 'enabled' in srv) or srv['enabled']: data = {"name": srv['name'], "comment": srv['comment'], "host": srv['host'], ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-24 09:43 Dave Page <[email protected]> parent: George Gelashvili <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-01-24 09:43 UTC (permalink / raw) To: George Gelashvili <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers On Fri, Jan 20, 2017 at 5:33 PM, George Gelashvili <[email protected]> wrote: > Thanks for bringing that to our attention! Here's the latest patch piranha:pgadmin4 dpage$ git apply ~/Downloads/acceptance-tests-with-server-start-and-polling.diff error: patch failed: web/regression/test_utils.py:69 error: web/regression/test_utils.py: patch does not apply :-( > On Fri, Jan 20, 2017 at 10:38 AM, Dave Page <[email protected]> wrote: >> >> On Thu, Jan 19, 2017 at 10:07 PM, George Gelashvili >> <[email protected]> wrote: >> > >> > Here is an updated patch which starts the server up when the test starts >> > and >> > uses the values from config.py for server name etc. It still requires >> > installing chromedriver before running. Should we add something to the >> > readme about that? >> >> Yes, we definitely should (including download site URL) >> >> > On Tue, Jan 17, 2017 at 11:09 AM, Atira Odhner <[email protected]> >> > wrote: >> >> >> >> Thanks for your feedback, Dave! >> >> >> >> We can put the tests under the regression directory. I think that makes >> >> sense. >> >> I'm not picturing these tests being module specific, but we may want to >> >> enable running it as a separate suite of tests. >> >> >> >> Thanks for the callout about the port and title. We'll make sure those >> >> are >> >> pulled from config or that the pgAdmin server is spun up by the test >> >> with >> >> specific values. >> >> >> >> I have a couple ideas about why the test might not have been running >> >> for >> >> you. I think the patch we attached didn't spin up its own pgAdmin yet >> >> and it >> >> definitely doesn't fill in username/password if your app is running >> >> that >> >> way. That's part of the WIP-ness :-P >> >> >> >> -Tira >> >> >> >> Hi >> >> >> >> On Thu, Jan 12, 2017 at 10:41 PM, George Gelashvili >> >> <ggelashvili(at)pivotal(dot)io> wrote: >> >> > here's the patch we forgot to attach. Also, you can see work on our >> >> > branch >> >> > at: >> >> > >> >> > >> >> > https://github.com/pivotalsoftware/pgadmin4/tree/pivotal/acceptance-tests >> >> > >> >> > On Thu, Jan 12, 2017 at 5:26 PM, George Gelashvili >> >> > <ggelashvili(at)pivotal(dot)io> >> >> > wrote: >> >> >> >> >> >> Hi there, >> >> >> >> >> >> We are working on browser-automation-based acceptance tests that >> >> >> exercise >> >> >> pgAdmin4 the way a user might. >> >> >> >> Nice! >> >> >> >> >> The first "connect to database" test works, but at the moment >> >> >> depends >> >> >> on >> >> >> Chrome and chromedriver. We would appreciate feedback on any >> >> >> possible >> >> >> license or code style issues at this point, as well as any thoughts >> >> >> on >> >> >> adding this sort of test to the codebase. >> >> >> >> A few thoughts: >> >> >> >> - If these tests are to run as part of the regression suite, the >> >> framework for them should live under that directory. >> >> >> >> - Are any of the tests likely to be module-specific? If so, they >> >> should really be part of the relevant module as the regression tests >> >> are. If they're more general/less tightly coupled, then I don't see a >> >> problem with them residing where they are. >> >> >> >> - Please take care not to include changes to .gitgnore files that >> >> aren't relevant to the rest of us. >> >> >> >> - The port number is hard-coded in the test. >> >> >> >> - You've hard-coded the string "pgAdmin 4". We've tried to keep that >> >> title as a config option in config.py, so you should pull the string >> >> from there rather than hard-coding it. >> >> >> >> - The connect test fails for me (Mac, Python 2.7). I have a suspicion >> >> that this may be because when the test starts chromedriver, OS X >> >> prompts the user about whether a listening port should be opened, but >> >> the tests don't wait (though, I tested with 3 servers configured and >> >> it failed with the same error on the second and third as well, long >> >> after I clicked OK on the prompt): >> >> >> >> Traceback (most recent call last): >> >> File >> >> >> >> "/Users/dpage/git/pgadmin4/web/acceptance/test_connects_to_database.py", >> >> line 32, in runTest >> >> self.assertEqual("pgAdmin 4", self.driver.title) >> >> AssertionError: 'pgAdmin 4' != u'localhost' >> >> >> >> - Please keep tests in the pgadmin. namespace (pgadmin.acceptance.??). >> >> >> >> - It looks like running a single test won't work yet (because of >> >> TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % >> >> arguments['pkg'])) >> >> >> >> Thanks! >> >> >> >> -- >> >> Dave Page >> >> Blog: http://pgsnake.blogspot.com >> >> Twitter: @pgsnake >> >> >> >> EnterpriseDB UK: http://www.enterprisedb.com >> >> The Enterprise PostgreSQL Company >> >> >> >> >> > >> > >> > >> > -- >> > Sent via pgadmin-hackers mailing list ([email protected]) >> > To make changes to your subscription: >> > http://www.postgresql.org/mailpref/pgadmin-hackers >> > >> >> >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company > > -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-25 23:31 George Gelashvili <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: George Gelashvili @ 2017-01-25 23:31 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers ah That diff was generated before the python 3 patch was applied. This should work against master Cheers, George On Tue, Jan 24, 2017 at 4:43 AM, Dave Page <[email protected]> wrote: > On Fri, Jan 20, 2017 at 5:33 PM, George Gelashvili > <[email protected]> wrote: > > Thanks for bringing that to our attention! Here's the latest patch > > piranha:pgadmin4 dpage$ git apply > ~/Downloads/acceptance-tests-with-server-start-and-polling.diff > error: patch failed: web/regression/test_utils.py:69 > error: web/regression/test_utils.py: patch does not apply > > :-( > > > On Fri, Jan 20, 2017 at 10:38 AM, Dave Page <[email protected]> wrote: > >> > >> On Thu, Jan 19, 2017 at 10:07 PM, George Gelashvili > >> <[email protected]> wrote: > >> > > >> > Here is an updated patch which starts the server up when the test > starts > >> > and > >> > uses the values from config.py for server name etc. It still requires > >> > installing chromedriver before running. Should we add something to the > >> > readme about that? > >> > >> Yes, we definitely should (including download site URL) > >> > >> > On Tue, Jan 17, 2017 at 11:09 AM, Atira Odhner <[email protected]> > >> > wrote: > >> >> > >> >> Thanks for your feedback, Dave! > >> >> > >> >> We can put the tests under the regression directory. I think that > makes > >> >> sense. > >> >> I'm not picturing these tests being module specific, but we may want > to > >> >> enable running it as a separate suite of tests. > >> >> > >> >> Thanks for the callout about the port and title. We'll make sure > those > >> >> are > >> >> pulled from config or that the pgAdmin server is spun up by the test > >> >> with > >> >> specific values. > >> >> > >> >> I have a couple ideas about why the test might not have been running > >> >> for > >> >> you. I think the patch we attached didn't spin up its own pgAdmin yet > >> >> and it > >> >> definitely doesn't fill in username/password if your app is running > >> >> that > >> >> way. That's part of the WIP-ness :-P > >> >> > >> >> -Tira > >> >> > >> >> Hi > >> >> > >> >> On Thu, Jan 12, 2017 at 10:41 PM, George Gelashvili > >> >> <ggelashvili(at)pivotal(dot)io> wrote: > >> >> > here's the patch we forgot to attach. Also, you can see work on our > >> >> > branch > >> >> > at: > >> >> > > >> >> > > >> >> > https://github.com/pivotalsoftware/pgadmin4/tree/ > pivotal/acceptance-tests > >> >> > > >> >> > On Thu, Jan 12, 2017 at 5:26 PM, George Gelashvili > >> >> > <ggelashvili(at)pivotal(dot)io> > >> >> > wrote: > >> >> >> > >> >> >> Hi there, > >> >> >> > >> >> >> We are working on browser-automation-based acceptance tests that > >> >> >> exercise > >> >> >> pgAdmin4 the way a user might. > >> >> > >> >> Nice! > >> >> > >> >> >> The first "connect to database" test works, but at the moment > >> >> >> depends > >> >> >> on > >> >> >> Chrome and chromedriver. We would appreciate feedback on any > >> >> >> possible > >> >> >> license or code style issues at this point, as well as any > thoughts > >> >> >> on > >> >> >> adding this sort of test to the codebase. > >> >> > >> >> A few thoughts: > >> >> > >> >> - If these tests are to run as part of the regression suite, the > >> >> framework for them should live under that directory. > >> >> > >> >> - Are any of the tests likely to be module-specific? If so, they > >> >> should really be part of the relevant module as the regression tests > >> >> are. If they're more general/less tightly coupled, then I don't see a > >> >> problem with them residing where they are. > >> >> > >> >> - Please take care not to include changes to .gitgnore files that > >> >> aren't relevant to the rest of us. > >> >> > >> >> - The port number is hard-coded in the test. > >> >> > >> >> - You've hard-coded the string "pgAdmin 4". We've tried to keep that > >> >> title as a config option in config.py, so you should pull the string > >> >> from there rather than hard-coding it. > >> >> > >> >> - The connect test fails for me (Mac, Python 2.7). I have a suspicion > >> >> that this may be because when the test starts chromedriver, OS X > >> >> prompts the user about whether a listening port should be opened, but > >> >> the tests don't wait (though, I tested with 3 servers configured and > >> >> it failed with the same error on the second and third as well, long > >> >> after I clicked OK on the prompt): > >> >> > >> >> Traceback (most recent call last): > >> >> File > >> >> > >> >> "/Users/dpage/git/pgadmin4/web/acceptance/test_connects_ > to_database.py", > >> >> line 32, in runTest > >> >> self.assertEqual("pgAdmin 4", self.driver.title) > >> >> AssertionError: 'pgAdmin 4' != u'localhost' > >> >> > >> >> - Please keep tests in the pgadmin. namespace > (pgadmin.acceptance.??). > >> >> > >> >> - It looks like running a single test won't work yet (because of > >> >> TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % > >> >> arguments['pkg'])) > >> >> > >> >> Thanks! > >> >> > >> >> -- > >> >> Dave Page > >> >> Blog: http://pgsnake.blogspot.com > >> >> Twitter: @pgsnake > >> >> > >> >> EnterpriseDB UK: http://www.enterprisedb.com > >> >> The Enterprise PostgreSQL Company > >> >> > >> >> > >> > > >> > > >> > > >> > -- > >> > Sent via pgadmin-hackers mailing list ([email protected] > ) > >> > To make changes to your subscription: > >> > http://www.postgresql.org/mailpref/pgadmin-hackers > >> > > >> > >> > >> > >> -- > >> Dave Page > >> Blog: http://pgsnake.blogspot.com > >> Twitter: @pgsnake > >> > >> EnterpriseDB UK: http://www.enterprisedb.com > >> The Enterprise PostgreSQL Company > > > > > > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > diff --git a/requirements_py2.txt b/requirements_py2.txt index 51170a45..de167121 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index f68db7a8..9565a6e4 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/test_connects_to_database.py b/web/pgadmin/acceptance/tests/test_connects_to_database.py new file mode 100644 index 00000000..c35cd0fd --- /dev/null +++ b/web/pgadmin/acceptance/tests/test_connects_to_database.py @@ -0,0 +1,127 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config + + +class ConnectsToDatabase(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + self.driver = webdriver.Chrome() + self.server_config = self.server + + print("opening browser") + self.driver.get("http://"; + config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT)) + self._wait_for_app() + + def runTest(self): + self.assertEqual(config.APP_NAME, self.driver.title) + self._wait_for_spinner_to_disappear() + + self._find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self._find_by_partial_link_text("Server...").click() + + self._fill_input_by_xpath("name", self.server_config['name']) + self._find_by_partial_link_text("Connection").click() + self._fill_input_by_xpath("host", self.server_config['host']) + self._fill_input_by_xpath("port", self.server_config['port']) + self._fill_input_by_xpath("username", self.server_config['username']) + self._fill_input_by_xpath("password", self.server_config['db_password']) + self._find_by_xpath("//button[contains(.,'Save')]").click() + + self._find_by_xpath("//*[@id='tree']//*[.='" + self.server_config['name'] + "']") + + def tearDown(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) + + def failureException(self, *args, **kwargs): + self.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) + + def _find_by_xpath(self, xpath): + return self._wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def _find_by_partial_link_text(self, link_text): + return self._wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def _fill_input_by_xpath(self, field_name, field_content): + self._find_by_xpath("//input[@name='" + field_name + "']").clear() + self._find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def _wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self.__wait_for("element to exist", element_if_it_exists) + + def _wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self.__wait_for("spinner to disappear", spinner_has_disappeared) + + def _wait_for_app(self): + def page_shows_app(): + if self.driver.title == config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self.__wait_for("app to start", page_shows_app) + + def __wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + self.fail("Timed out waiting for " + waiting_for_message) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py index f501d3d0..99fa00c7 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py @@ -1673,8 +1673,8 @@ class TableView(PGChildNodeView, DataTypeReader, VacuumSettings): return make_json_response( success=1, - info=gettext("Trigger(s) have been enabled") if is_enable - else gettext("Trigger(s) have been disabled"), + info=gettext("Trigger(s) has been enabled") if is_enable + else gettext("Trigger(s) has been disabled"), data={ 'id': tid, 'scid': scid @@ -1706,7 +1706,7 @@ class TableView(PGChildNodeView, DataTypeReader, VacuumSettings): return make_json_response( success=1, - info=gettext("Table statistics have been reset"), + info=gettext("Table statistics has been reset"), data={ 'id': tid, 'scid': scid diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py index fe7d9349..6b7f52b0 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py @@ -816,7 +816,7 @@ class ExclusionConstraintView(PGChildNodeView): sql = render_template("/".join([self.template_path, 'create.sql']), data=data, conn=self.conn) - return sql, data['name'] + return sql, data['name'] if 'name' in data else old_data['name'] @check_precondition def sql(self, gid, sid, did, scid, tid, exid=None): diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js index e7bd905c..cfe15195 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js @@ -7,11 +7,10 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { defaults: { column: undefined, oper_class: undefined, - order: false, - nulls_order: false, + order: undefined, + nulls_order: undefined, operator:undefined, - col_type:undefined, - is_sort_nulls_applicable: true + col_type:undefined }, toJSON: function () { var d = pgBrowser.Node.Model.prototype.toJSON.apply(this, arguments); @@ -24,9 +23,26 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { },{ id: 'oper_class', label:'{{ _('Operator class') }}', type:'text', node: 'table', url: 'get_oper_class', first_empty: true, - editable: true, + editable: function(m) { + if (m instanceof Backbone.Collection) { + return true; + } + if ((_.has(m.collection, 'handler') && + !_.isUndefined(m.collection.handler) && + !_.isUndefined(m.collection.handler.get('oid')))) { + return false; + } + + if (m.collection) { + var indexType = m.collection.handler.get('amname') + return (indexType == 'btree' || _.isUndefined(indexType) || + _.isNull(indexType) || indexType == ''); + } else { + return true; + } + }, select2: { - allowClear: true, width: 'style', tags: true, + allowClear: true, width: 'style', placeholder: '{{ _("Select the operator class") }}' }, cell: Backgrid.Extension.Select2Cell.extend({ initialize: function () { @@ -39,12 +55,6 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { if (url && (indextype == 'btree' || _.isUndefined(indextype) || _.isNull(indextype) || indextype == '')) { - // Set sort_order and nulls to true if access method is btree - setTimeout(function() { - m.set('order', true); - m.set('nulls_order', true); - }, 10); - var node = this.column.get('schema_node'), eventHandler = m.top || m, node_info = this.column.get('node_info'), @@ -98,14 +108,6 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { if (m instanceof Backbone.Collection) { return true; } - else { - if (m.top.get('amname') === 'btree') { - m.set('is_sort_nulls_applicable', true); - return true; - } - m.set('is_sort_nulls_applicable', false); - return false; - } if ((_.has(m.collection, 'handler') && !_.isUndefined(m.collection.handler) && !_.isUndefined(m.collection.handler.get('oid')))) { @@ -122,15 +124,6 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { if (m instanceof Backbone.Collection) { return true; } - else { - if (m.top.get('amname') === 'btree') { - m.set('is_sort_nulls_applicable', true); - return true; - } - m.set('is_sort_nulls_applicable', false); - return false; - } - if ((_.has(m.collection, 'handler') && !_.isUndefined(m.collection.handler) && !_.isUndefined(m.collection.handler.get('oid')))) { @@ -905,15 +898,8 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { }], validate: function() { this.errorModel.clear(); - var columns = this.get('columns'), - name = this.get('name'); - - if ((_.isUndefined(name) || _.isNull(name) || name.length < 1)) { - var msg = '{{ _('Please specify name for exclusion constraint.') }}'; - this.errorModel.set('name', msg); - return msg; - } - else if ((_.isUndefined(columns) || _.isNull(columns) || columns.length < 1)) { + var columns = this.get('columns'); + if ((_.isUndefined(columns) || _.isNull(columns) || columns.length < 1)) { var msg = '{{ _('Please specify columns for exclusion constraint.') }}'; this.errorModel.set('columns', msg); return msg; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js index 6f462652..6531ba5e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js @@ -51,8 +51,7 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) { collspcname: undefined, op_class: undefined, sort_order: false, - nulls: false, - is_sort_nulls_applicable: true + nulls: false }, schema: [ { @@ -78,7 +77,7 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) { control: 'node-ajax-options', url: 'get_collations', node: 'index' },{ id: 'op_class', label:'{{ _('Operator class') }}', - cell: NodeAjaxOptionsDepsCell, tags: true, + cell: NodeAjaxOptionsDepsCell, type: 'text', disabled: 'checkAccessMethod', editable: function(m) { // Header cell then skip @@ -109,19 +108,13 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) { },{ id: 'sort_order', label:'{{ _('Sort order') }}', cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch', + disabled: 'checkAccessMethod', editable: function(m) { - // Header cell then skip - if (m instanceof Backbone.Collection) { - return false; - } - else { - if (m.top.get('amname') === 'btree') { - m.set('is_sort_nulls_applicable', true); - return true; + // Header cell then skip + if (m instanceof Backbone.Collection) { + return false; } - m.set('is_sort_nulls_applicable', false); - return false; - } + return !(m.checkAccessMethod.apply(this, arguments)); }, deps: ['amname'], options: { @@ -132,18 +125,13 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) { },{ id: 'nulls', label:'{{ _('NULLs') }}', cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch', + disabled: 'checkAccessMethod', editable: function(m) { - // Header cell then skip - if (m instanceof Backbone.Collection) { - return true; - } else { - if (m.top.get('amname') === 'btree') { - m.set('is_sort_nulls_applicable', true); + // Header cell then skip + if (m instanceof Backbone.Collection) { return true; - } - m.set('is_sort_nulls_applicable', false); - return false; - } + } + return !(m.checkAccessMethod.apply(this, arguments)); }, deps: ['amname', 'sort_order'], options: { @@ -196,11 +184,9 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) { if(m.get('sort_order') == true && m.previous('sort_order') == false) { setTimeout(function() { m.set('nulls', true) }, 10); } + return false; } - else { - m.set('is_sort_nulls_applicable', false); - } - return false; + return true; }, }); diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql index 6d0bd1be..db29048b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql @@ -1,7 +1,7 @@ ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }} ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} ( {% for col in data.columns %}{% if loop.index != 1 %}, - {% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} + {% endif %}{{ conn|qtIdent(col.column)}} {% if col.oper_class and col.oper_class != '' %}{{col.oper_class}} {% endif%}{% if col.order %}ASC{% else %}DESC{% endif %} NULLS {% if col.nulls_order %}FIRST{% else %}LAST{% endif %} WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %} USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql index 6d0bd1be..db29048b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql @@ -1,7 +1,7 @@ ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }} ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} ( {% for col in data.columns %}{% if loop.index != 1 %}, - {% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} + {% endif %}{{ conn|qtIdent(col.column)}} {% if col.oper_class and col.oper_class != '' %}{{col.oper_class}} {% endif%}{% if col.order %}ASC{% else %}DESC{% endif %} NULLS {% if col.nulls_order %}FIRST{% else %}LAST{% endif %} WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %} USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql index 6d0bd1be..db29048b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql @@ -1,7 +1,7 @@ ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }} ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} ( {% for col in data.columns %}{% if loop.index != 1 %}, - {% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} + {% endif %}{{ conn|qtIdent(col.column)}} {% if col.oper_class and col.oper_class != '' %}{{col.oper_class}} {% endif%}{% if col.order %}ASC{% else %}DESC{% endif %} NULLS {% if col.nulls_order %}FIRST{% else %}LAST{% endif %} WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %} USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql index b7bfa527..33af1973 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql @@ -3,7 +3,7 @@ CREATE {% if data.indisunique %}UNIQUE {% endif %}INDEX {% if data.isconcurrent {% if mode == 'create' %} ({% for c in data.columns %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(c.colname)}}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.op_class %} - {{c.op_class}}{% endif %}{% if data.amname is defined %}{% if c.sort_order is defined and c.is_sort_nulls_applicable %}{% if c.sort_order %} DESC{% else %} ASC{% endif %}{% endif %}{% if c.nulls is defined and c.is_sort_nulls_applicable %} NULLS {% if c.nulls %} + {{c.op_class}}{% endif %}{% if data.amname is defined and data.amname not in ['gist', 'gin'] %}{% if c.sort_order is defined %}{% if c.sort_order %} DESC{% else %} ASC{% endif %}{% endif %}{% if c.nulls is defined %} NULLS {% if c.nulls %} FIRST{% else %}LAST{% endif %}{% endif %}{% endif %}{% endfor %}) {% else %} {## We will get indented data from postgres for column ##} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js index c87a2930..2d006090 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js @@ -198,54 +198,51 @@ function($, _, S, pgAdmin, pgBrowser, alertify) { }); }, reset_table_stats: function(args) { - var input = args || {}, - obj = this, - t = pgBrowser.tree, - i = input.item || t.selected(), - d = i && i.length == 1 ? t.itemData(i) : undefined; + var input = args || {}; + obj = this, + t = pgBrowser.tree, + i = input.item || t.selected(), + d = i && i.length == 1 ? t.itemData(i) : undefined; if (!d) return false; alertify.confirm( - '{{ _('Reset statistics') }}', - S('{{ _('Are you sure you want to reset the statistics for table %%s?') }}').sprintf(d._label).value(), + S('{{ _('Are you sure you want to reset table statistics for %s?') }}').sprintf(d.label).value(), function (e) { - if (e) { - var data = d; - $.ajax({ - url: obj.generate_url(i, 'reset' , d, true), - type:'DELETE', - success: function(res) { - if (res.success == 1) { - alertify.success("{{ _('" + res.info + "') }}"); - t.removeIcon(i); - data.icon = 'icon-table'; - t.addIcon(i, {icon: data.icon}); - t.unload(i); - t.setInode(i); - t.deselect(i); - // Fetch updated data from server - setTimeout(function() { - t.select(i); - }, 10); - } - }, - error: function(xhr, status, error) { - try { - var err = $.parseJSON(xhr.responseText); - if (err.success == 0) { - msg = S('{{ _(' + err.errormsg + ')}}').value(); - alertify.error("{{ _('" + err.errormsg + "') }}"); - } - } catch (e) {} + if (e) { + var data = d; + $.ajax({ + url: obj.generate_url(i, 'reset' , d, true), + type:'DELETE', + success: function(res) { + if (res.success == 1) { + alertify.success("{{ _('" + res.info + "') }}"); + t.removeIcon(i); + data.icon = 'icon-table'; + t.addIcon(i, {icon: data.icon}); t.unload(i); + t.setInode(i); + t.deselect(i); + // Fetch updated data from server + setTimeout(function() { + t.select(i); + }, 10); } - }); - } - }, - function() {} - ); + }, + error: function(xhr, status, error) { + try { + var err = $.parseJSON(xhr.responseText); + if (err.success == 0) { + msg = S('{{ _(' + err.errormsg + ')}}').value(); + alertify.error("{{ _('" + err.errormsg + "') }}"); + } + } catch (e) {} + t.unload(i); + } + }); + } + }); } }, model: pgBrowser.Node.Model.extend({ diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..fed26a0f 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, *pkgs): cls.registry = dict() + all_modules = [] + + for pkg in pkgs: + all_modules += find_modules(pkg, False, True) + + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): + for module_name in all_modules: + if config.SERVER_MODE: try: if "tests." in str(module_name): import_module(module_name) except ImportError: traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): + else: try: # Exclude the test cases in browser node if SERVER_MODE # is False diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..ae5d268d 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [text/plain] acceptance-tests-with-server-start-and-polling.diff (29.1K, 3-acceptance-tests-with-server-start-and-polling.diff) download | inline diff: diff --git a/requirements_py2.txt b/requirements_py2.txt index 51170a45..de167121 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index f68db7a8..9565a6e4 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/test_connects_to_database.py b/web/pgadmin/acceptance/tests/test_connects_to_database.py new file mode 100644 index 00000000..c35cd0fd --- /dev/null +++ b/web/pgadmin/acceptance/tests/test_connects_to_database.py @@ -0,0 +1,127 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config + + +class ConnectsToDatabase(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + self.driver = webdriver.Chrome() + self.server_config = self.server + + print("opening browser") + self.driver.get("http://" + config.DEFAULT_SERVER + ":" + str(config.DEFAULT_SERVER_PORT)) + self._wait_for_app() + + def runTest(self): + self.assertEqual(config.APP_NAME, self.driver.title) + self._wait_for_spinner_to_disappear() + + self._find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self._find_by_partial_link_text("Server...").click() + + self._fill_input_by_xpath("name", self.server_config['name']) + self._find_by_partial_link_text("Connection").click() + self._fill_input_by_xpath("host", self.server_config['host']) + self._fill_input_by_xpath("port", self.server_config['port']) + self._fill_input_by_xpath("username", self.server_config['username']) + self._fill_input_by_xpath("password", self.server_config['db_password']) + self._find_by_xpath("//button[contains(.,'Save')]").click() + + self._find_by_xpath("//*[@id='tree']//*[.='" + self.server_config['name'] + "']") + + def tearDown(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) + + def failureException(self, *args, **kwargs): + self.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) + + def _find_by_xpath(self, xpath): + return self._wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def _find_by_partial_link_text(self, link_text): + return self._wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def _fill_input_by_xpath(self, field_name, field_content): + self._find_by_xpath("//input[@name='" + field_name + "']").clear() + self._find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def _wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self.__wait_for("element to exist", element_if_it_exists) + + def _wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self.__wait_for("spinner to disappear", spinner_has_disappeared) + + def _wait_for_app(self): + def page_shows_app(): + if self.driver.title == config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self.__wait_for("app to start", page_shows_app) + + def __wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + self.fail("Timed out waiting for " + waiting_for_message) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py index f501d3d0..99fa00c7 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py @@ -1673,8 +1673,8 @@ class TableView(PGChildNodeView, DataTypeReader, VacuumSettings): return make_json_response( success=1, - info=gettext("Trigger(s) have been enabled") if is_enable - else gettext("Trigger(s) have been disabled"), + info=gettext("Trigger(s) has been enabled") if is_enable + else gettext("Trigger(s) has been disabled"), data={ 'id': tid, 'scid': scid @@ -1706,7 +1706,7 @@ class TableView(PGChildNodeView, DataTypeReader, VacuumSettings): return make_json_response( success=1, - info=gettext("Table statistics have been reset"), + info=gettext("Table statistics has been reset"), data={ 'id': tid, 'scid': scid diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py index fe7d9349..6b7f52b0 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/__init__.py @@ -816,7 +816,7 @@ class ExclusionConstraintView(PGChildNodeView): sql = render_template("/".join([self.template_path, 'create.sql']), data=data, conn=self.conn) - return sql, data['name'] + return sql, data['name'] if 'name' in data else old_data['name'] @check_precondition def sql(self, gid, sid, did, scid, tid, exid=None): diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js index e7bd905c..cfe15195 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/templates/exclusion_constraint/js/exclusion_constraint.js @@ -7,11 +7,10 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { defaults: { column: undefined, oper_class: undefined, - order: false, - nulls_order: false, + order: undefined, + nulls_order: undefined, operator:undefined, - col_type:undefined, - is_sort_nulls_applicable: true + col_type:undefined }, toJSON: function () { var d = pgBrowser.Node.Model.prototype.toJSON.apply(this, arguments); @@ -24,9 +23,26 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { },{ id: 'oper_class', label:'{{ _('Operator class') }}', type:'text', node: 'table', url: 'get_oper_class', first_empty: true, - editable: true, + editable: function(m) { + if (m instanceof Backbone.Collection) { + return true; + } + if ((_.has(m.collection, 'handler') && + !_.isUndefined(m.collection.handler) && + !_.isUndefined(m.collection.handler.get('oid')))) { + return false; + } + + if (m.collection) { + var indexType = m.collection.handler.get('amname') + return (indexType == 'btree' || _.isUndefined(indexType) || + _.isNull(indexType) || indexType == ''); + } else { + return true; + } + }, select2: { - allowClear: true, width: 'style', tags: true, + allowClear: true, width: 'style', placeholder: '{{ _("Select the operator class") }}' }, cell: Backgrid.Extension.Select2Cell.extend({ initialize: function () { @@ -39,12 +55,6 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { if (url && (indextype == 'btree' || _.isUndefined(indextype) || _.isNull(indextype) || indextype == '')) { - // Set sort_order and nulls to true if access method is btree - setTimeout(function() { - m.set('order', true); - m.set('nulls_order', true); - }, 10); - var node = this.column.get('schema_node'), eventHandler = m.top || m, node_info = this.column.get('node_info'), @@ -98,14 +108,6 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { if (m instanceof Backbone.Collection) { return true; } - else { - if (m.top.get('amname') === 'btree') { - m.set('is_sort_nulls_applicable', true); - return true; - } - m.set('is_sort_nulls_applicable', false); - return false; - } if ((_.has(m.collection, 'handler') && !_.isUndefined(m.collection.handler) && !_.isUndefined(m.collection.handler.get('oid')))) { @@ -122,15 +124,6 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { if (m instanceof Backbone.Collection) { return true; } - else { - if (m.top.get('amname') === 'btree') { - m.set('is_sort_nulls_applicable', true); - return true; - } - m.set('is_sort_nulls_applicable', false); - return false; - } - if ((_.has(m.collection, 'handler') && !_.isUndefined(m.collection.handler) && !_.isUndefined(m.collection.handler.get('oid')))) { @@ -905,15 +898,8 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { }], validate: function() { this.errorModel.clear(); - var columns = this.get('columns'), - name = this.get('name'); - - if ((_.isUndefined(name) || _.isNull(name) || name.length < 1)) { - var msg = '{{ _('Please specify name for exclusion constraint.') }}'; - this.errorModel.set('name', msg); - return msg; - } - else if ((_.isUndefined(columns) || _.isNull(columns) || columns.length < 1)) { + var columns = this.get('columns'); + if ((_.isUndefined(columns) || _.isNull(columns) || columns.length < 1)) { var msg = '{{ _('Please specify columns for exclusion constraint.') }}'; this.errorModel.set('columns', msg); return msg; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js index 6f462652..6531ba5e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/templates/index/js/index.js @@ -51,8 +51,7 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) { collspcname: undefined, op_class: undefined, sort_order: false, - nulls: false, - is_sort_nulls_applicable: true + nulls: false }, schema: [ { @@ -78,7 +77,7 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) { control: 'node-ajax-options', url: 'get_collations', node: 'index' },{ id: 'op_class', label:'{{ _('Operator class') }}', - cell: NodeAjaxOptionsDepsCell, tags: true, + cell: NodeAjaxOptionsDepsCell, type: 'text', disabled: 'checkAccessMethod', editable: function(m) { // Header cell then skip @@ -109,19 +108,13 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) { },{ id: 'sort_order', label:'{{ _('Sort order') }}', cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch', + disabled: 'checkAccessMethod', editable: function(m) { - // Header cell then skip - if (m instanceof Backbone.Collection) { - return false; - } - else { - if (m.top.get('amname') === 'btree') { - m.set('is_sort_nulls_applicable', true); - return true; + // Header cell then skip + if (m instanceof Backbone.Collection) { + return false; } - m.set('is_sort_nulls_applicable', false); - return false; - } + return !(m.checkAccessMethod.apply(this, arguments)); }, deps: ['amname'], options: { @@ -132,18 +125,13 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) { },{ id: 'nulls', label:'{{ _('NULLs') }}', cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch', + disabled: 'checkAccessMethod', editable: function(m) { - // Header cell then skip - if (m instanceof Backbone.Collection) { - return true; - } else { - if (m.top.get('amname') === 'btree') { - m.set('is_sort_nulls_applicable', true); + // Header cell then skip + if (m instanceof Backbone.Collection) { return true; - } - m.set('is_sort_nulls_applicable', false); - return false; - } + } + return !(m.checkAccessMethod.apply(this, arguments)); }, deps: ['amname', 'sort_order'], options: { @@ -196,11 +184,9 @@ function($, _, S, pgAdmin, pgBrowser, Backform, alertify) { if(m.get('sort_order') == true && m.previous('sort_order') == false) { setTimeout(function() { m.set('nulls', true) }, 10); } + return false; } - else { - m.set('is_sort_nulls_applicable', false); - } - return false; + return true; }, }); diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql index 6d0bd1be..db29048b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.1_plus/create.sql @@ -1,7 +1,7 @@ ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }} ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} ( {% for col in data.columns %}{% if loop.index != 1 %}, - {% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} + {% endif %}{{ conn|qtIdent(col.column)}} {% if col.oper_class and col.oper_class != '' %}{{col.oper_class}} {% endif%}{% if col.order %}ASC{% else %}DESC{% endif %} NULLS {% if col.nulls_order %}FIRST{% else %}LAST{% endif %} WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %} USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql index 6d0bd1be..db29048b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.2_plus/create.sql @@ -1,7 +1,7 @@ ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }} ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} ( {% for col in data.columns %}{% if loop.index != 1 %}, - {% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} + {% endif %}{{ conn|qtIdent(col.column)}} {% if col.oper_class and col.oper_class != '' %}{{col.oper_class}} {% endif%}{% if col.order %}ASC{% else %}DESC{% endif %} NULLS {% if col.nulls_order %}FIRST{% else %}LAST{% endif %} WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %} USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql index 6d0bd1be..db29048b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/9.6_plus/create.sql @@ -1,7 +1,7 @@ ALTER TABLE {{ conn|qtIdent(data.schema, data.table) }} ADD{% if data.name %} CONSTRAINT {{ conn|qtIdent(data.name) }}{% endif%} EXCLUDE {% if data.amname and data.amname != '' %}USING {{data.amname}}{% endif %} ( {% for col in data.columns %}{% if loop.index != 1 %}, - {% endif %}{{ conn|qtIdent(col.column)}}{% if col.oper_class and col.oper_class != '' %} {{col.oper_class}}{% endif%}{% if col.order is defined and col.is_sort_nulls_applicable %}{% if col.order %} ASC{% else %} DESC{% endif %} NULLS{% endif %} {% if col.nulls_order is defined and col.is_sort_nulls_applicable %}{% if col.nulls_order %}FIRST {% else %}LAST {% endif %}{% endif %}WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} + {% endif %}{{ conn|qtIdent(col.column)}} {% if col.oper_class and col.oper_class != '' %}{{col.oper_class}} {% endif%}{% if col.order %}ASC{% else %}DESC{% endif %} NULLS {% if col.nulls_order %}FIRST{% else %}LAST{% endif %} WITH {{col.operator}}{% endfor %}){% if data.fillfactor %} WITH (FILLFACTOR={{data.fillfactor}}){% endif %}{% if data.spcname and data.spcname != "pg_default" %} USING INDEX TABLESPACE {{ conn|qtIdent(data.spcname) }}{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql index b7bfa527..33af1973 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/index/sql/9.1_plus/create.sql @@ -3,7 +3,7 @@ CREATE {% if data.indisunique %}UNIQUE {% endif %}INDEX {% if data.isconcurrent {% if mode == 'create' %} ({% for c in data.columns %}{% if loop.index != 1 %}, {% endif %}{{conn|qtIdent(c.colname)}}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.op_class %} - {{c.op_class}}{% endif %}{% if data.amname is defined %}{% if c.sort_order is defined and c.is_sort_nulls_applicable %}{% if c.sort_order %} DESC{% else %} ASC{% endif %}{% endif %}{% if c.nulls is defined and c.is_sort_nulls_applicable %} NULLS {% if c.nulls %} + {{c.op_class}}{% endif %}{% if data.amname is defined and data.amname not in ['gist', 'gin'] %}{% if c.sort_order is defined %}{% if c.sort_order %} DESC{% else %} ASC{% endif %}{% endif %}{% if c.nulls is defined %} NULLS {% if c.nulls %} FIRST{% else %}LAST{% endif %}{% endif %}{% endif %}{% endfor %}) {% else %} {## We will get indented data from postgres for column ##} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js index c87a2930..2d006090 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/js/table.js @@ -198,54 +198,51 @@ function($, _, S, pgAdmin, pgBrowser, alertify) { }); }, reset_table_stats: function(args) { - var input = args || {}, - obj = this, - t = pgBrowser.tree, - i = input.item || t.selected(), - d = i && i.length == 1 ? t.itemData(i) : undefined; + var input = args || {}; + obj = this, + t = pgBrowser.tree, + i = input.item || t.selected(), + d = i && i.length == 1 ? t.itemData(i) : undefined; if (!d) return false; alertify.confirm( - '{{ _('Reset statistics') }}', - S('{{ _('Are you sure you want to reset the statistics for table %%s?') }}').sprintf(d._label).value(), + S('{{ _('Are you sure you want to reset table statistics for %s?') }}').sprintf(d.label).value(), function (e) { - if (e) { - var data = d; - $.ajax({ - url: obj.generate_url(i, 'reset' , d, true), - type:'DELETE', - success: function(res) { - if (res.success == 1) { - alertify.success("{{ _('" + res.info + "') }}"); - t.removeIcon(i); - data.icon = 'icon-table'; - t.addIcon(i, {icon: data.icon}); - t.unload(i); - t.setInode(i); - t.deselect(i); - // Fetch updated data from server - setTimeout(function() { - t.select(i); - }, 10); - } - }, - error: function(xhr, status, error) { - try { - var err = $.parseJSON(xhr.responseText); - if (err.success == 0) { - msg = S('{{ _(' + err.errormsg + ')}}').value(); - alertify.error("{{ _('" + err.errormsg + "') }}"); - } - } catch (e) {} + if (e) { + var data = d; + $.ajax({ + url: obj.generate_url(i, 'reset' , d, true), + type:'DELETE', + success: function(res) { + if (res.success == 1) { + alertify.success("{{ _('" + res.info + "') }}"); + t.removeIcon(i); + data.icon = 'icon-table'; + t.addIcon(i, {icon: data.icon}); t.unload(i); + t.setInode(i); + t.deselect(i); + // Fetch updated data from server + setTimeout(function() { + t.select(i); + }, 10); } - }); - } - }, - function() {} - ); + }, + error: function(xhr, status, error) { + try { + var err = $.parseJSON(xhr.responseText); + if (err.success == 0) { + msg = S('{{ _(' + err.errormsg + ')}}').value(); + alertify.error("{{ _('" + err.errormsg + "') }}"); + } + } catch (e) {} + t.unload(i); + } + }); + } + }); } }, model: pgBrowser.Node.Model.extend({ diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..fed26a0f 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, *pkgs): cls.registry = dict() + all_modules = [] + + for pkg in pkgs: + all_modules += find_modules(pkg, False, True) + + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): + for module_name in all_modules: + if config.SERVER_MODE: try: if "tests." in str(module_name): import_module(module_name) except ImportError: traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): + else: try: # Exclude the test cases in browser node if SERVER_MODE # is False diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..ae5d268d 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-26 22:40 George Gelashvili <[email protected]> parent: George Gelashvili <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: George Gelashvili @ 2017-01-26 22:40 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers instead of that patch, please use this no-zombies version that kills the started process group instead of pid-only. On Wed, Jan 25, 2017 at 6:31 PM, George Gelashvili <[email protected]> wrote: > ah > That diff was generated before the python 3 patch was applied. This should > work against master > > Cheers, > George > > On Tue, Jan 24, 2017 at 4:43 AM, Dave Page <[email protected]> wrote: > >> On Fri, Jan 20, 2017 at 5:33 PM, George Gelashvili >> <[email protected]> wrote: >> > Thanks for bringing that to our attention! Here's the latest patch >> >> piranha:pgadmin4 dpage$ git apply >> ~/Downloads/acceptance-tests-with-server-start-and-polling.diff >> error: patch failed: web/regression/test_utils.py:69 >> error: web/regression/test_utils.py: patch does not apply >> >> :-( >> >> > On Fri, Jan 20, 2017 at 10:38 AM, Dave Page <[email protected]> wrote: >> >> >> >> On Thu, Jan 19, 2017 at 10:07 PM, George Gelashvili >> >> <[email protected]> wrote: >> >> > >> >> > Here is an updated patch which starts the server up when the test >> starts >> >> > and >> >> > uses the values from config.py for server name etc. It still requires >> >> > installing chromedriver before running. Should we add something to >> the >> >> > readme about that? >> >> >> >> Yes, we definitely should (including download site URL) >> >> >> >> > On Tue, Jan 17, 2017 at 11:09 AM, Atira Odhner <[email protected]> >> >> > wrote: >> >> >> >> >> >> Thanks for your feedback, Dave! >> >> >> >> >> >> We can put the tests under the regression directory. I think that >> makes >> >> >> sense. >> >> >> I'm not picturing these tests being module specific, but we may >> want to >> >> >> enable running it as a separate suite of tests. >> >> >> >> >> >> Thanks for the callout about the port and title. We'll make sure >> those >> >> >> are >> >> >> pulled from config or that the pgAdmin server is spun up by the test >> >> >> with >> >> >> specific values. >> >> >> >> >> >> I have a couple ideas about why the test might not have been running >> >> >> for >> >> >> you. I think the patch we attached didn't spin up its own pgAdmin >> yet >> >> >> and it >> >> >> definitely doesn't fill in username/password if your app is running >> >> >> that >> >> >> way. That's part of the WIP-ness :-P >> >> >> >> >> >> -Tira >> >> >> >> >> >> Hi >> >> >> >> >> >> On Thu, Jan 12, 2017 at 10:41 PM, George Gelashvili >> >> >> <ggelashvili(at)pivotal(dot)io> wrote: >> >> >> > here's the patch we forgot to attach. Also, you can see work on >> our >> >> >> > branch >> >> >> > at: >> >> >> > >> >> >> > >> >> >> > https://github.com/pivotalsoftware/pgadmin4/tree/pivotal/ >> acceptance-tests >> >> >> > >> >> >> > On Thu, Jan 12, 2017 at 5:26 PM, George Gelashvili >> >> >> > <ggelashvili(at)pivotal(dot)io> >> >> >> > wrote: >> >> >> >> >> >> >> >> Hi there, >> >> >> >> >> >> >> >> We are working on browser-automation-based acceptance tests that >> >> >> >> exercise >> >> >> >> pgAdmin4 the way a user might. >> >> >> >> >> >> Nice! >> >> >> >> >> >> >> The first "connect to database" test works, but at the moment >> >> >> >> depends >> >> >> >> on >> >> >> >> Chrome and chromedriver. We would appreciate feedback on any >> >> >> >> possible >> >> >> >> license or code style issues at this point, as well as any >> thoughts >> >> >> >> on >> >> >> >> adding this sort of test to the codebase. >> >> >> >> >> >> A few thoughts: >> >> >> >> >> >> - If these tests are to run as part of the regression suite, the >> >> >> framework for them should live under that directory. >> >> >> >> >> >> - Are any of the tests likely to be module-specific? If so, they >> >> >> should really be part of the relevant module as the regression tests >> >> >> are. If they're more general/less tightly coupled, then I don't see >> a >> >> >> problem with them residing where they are. >> >> >> >> >> >> - Please take care not to include changes to .gitgnore files that >> >> >> aren't relevant to the rest of us. >> >> >> >> >> >> - The port number is hard-coded in the test. >> >> >> >> >> >> - You've hard-coded the string "pgAdmin 4". We've tried to keep that >> >> >> title as a config option in config.py, so you should pull the string >> >> >> from there rather than hard-coding it. >> >> >> >> >> >> - The connect test fails for me (Mac, Python 2.7). I have a >> suspicion >> >> >> that this may be because when the test starts chromedriver, OS X >> >> >> prompts the user about whether a listening port should be opened, >> but >> >> >> the tests don't wait (though, I tested with 3 servers configured and >> >> >> it failed with the same error on the second and third as well, long >> >> >> after I clicked OK on the prompt): >> >> >> >> >> >> Traceback (most recent call last): >> >> >> File >> >> >> >> >> >> "/Users/dpage/git/pgadmin4/web/acceptance/test_connects_to_ >> database.py", >> >> >> line 32, in runTest >> >> >> self.assertEqual("pgAdmin 4", self.driver.title) >> >> >> AssertionError: 'pgAdmin 4' != u'localhost' >> >> >> >> >> >> - Please keep tests in the pgadmin. namespace >> (pgadmin.acceptance.??). >> >> >> >> >> >> - It looks like running a single test won't work yet (because of >> >> >> TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % >> >> >> arguments['pkg'])) >> >> >> >> >> >> Thanks! >> >> >> >> >> >> -- >> >> >> Dave Page >> >> >> Blog: http://pgsnake.blogspot.com >> >> >> Twitter: @pgsnake >> >> >> >> >> >> EnterpriseDB UK: http://www.enterprisedb.com >> >> >> The Enterprise PostgreSQL Company >> >> >> >> >> >> >> >> > >> >> > >> >> > >> >> > -- >> >> > Sent via pgadmin-hackers mailing list ([email protected] >> g) >> >> > To make changes to your subscription: >> >> > http://www.postgresql.org/mailpref/pgadmin-hackers >> >> > >> >> >> >> >> >> >> >> -- >> >> Dave Page >> >> Blog: http://pgsnake.blogspot.com >> >> Twitter: @pgsnake >> >> >> >> EnterpriseDB UK: http://www.enterprisedb.com >> >> The Enterprise PostgreSQL Company >> > >> > >> >> >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> > > diff --git a/requirements_py2.txt b/requirements_py2.txt index 51170a45..de167121 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index f68db7a8..9565a6e4 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_database_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_database_feature_test.py new file mode 100644 index 00000000..a3baa45c --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_database_feature_test.py @@ -0,0 +1,78 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config as app_config +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToDatabaseFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def tearDown(self): + self.page.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..fed26a0f 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, *pkgs): cls.registry = dict() + all_modules = [] + + for pkg in pkgs: + all_modules += find_modules(pkg, False, True) + + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): + for module_name in all_modules: + if config.SERVER_MODE: try: if "tests." in str(module_name): import_module(module_name) except ImportError: traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): + else: try: # Exclude the test cases in browser node if SERVER_MODE # is False diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..ae5d268d 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..68f36cbc 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,24 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: diff --git a/web/regression/utils/__init__.py b/web/regression/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..c093f046 --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,24 @@ +import os +import subprocess + +import signal + + +class AppStarter: + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + print("opening browser") + self.driver.get("http://"; + self.app_config.DEFAULT_SERVER + ":" + str(self.app_config.DEFAULT_SERVER_PORT)) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..1c8857f2 --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,103 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + + +class PgadminPage: + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + self.find_by_xpath("//input[@name='" + field_name + "']").clear() + self.find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [text/plain] acceptance-test-no-zombies.diff (13.0K, 3-acceptance-test-no-zombies.diff) download | inline diff: diff --git a/requirements_py2.txt b/requirements_py2.txt index 51170a45..de167121 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index f68db7a8..9565a6e4 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_database_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_database_feature_test.py new file mode 100644 index 00000000..a3baa45c --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_database_feature_test.py @@ -0,0 +1,78 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config as app_config +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToDatabaseFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], shell=False, preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def tearDown(self): + self.page.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..fed26a0f 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,20 +54,25 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, *pkgs): cls.registry = dict() + all_modules = [] + + for pkg in pkgs: + all_modules += find_modules(pkg, False, True) + + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): + for module_name in all_modules: + if config.SERVER_MODE: try: if "tests." in str(module_name): import_module(module_name) except ImportError: traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): + else: try: # Exclude the test cases in browser node if SERVER_MODE # is False diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..ae5d268d 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..68f36cbc 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,24 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: diff --git a/web/regression/utils/__init__.py b/web/regression/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..c093f046 --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,24 @@ +import os +import subprocess + +import signal + + +class AppStarter: + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + print("opening browser") + self.driver.get("http://" + self.app_config.DEFAULT_SERVER + ":" + str(self.app_config.DEFAULT_SERVER_PORT)) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..1c8857f2 --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,103 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + + +class PgadminPage: + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + self.find_by_xpath("//input[@name='" + field_name + "']").clear() + self.find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-27 16:11 Dave Page <[email protected]> parent: George Gelashvili <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-01-27 16:11 UTC (permalink / raw) To: George Gelashvili <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers On Thu, Jan 26, 2017 at 10:40 PM, George Gelashvili <[email protected]> wrote: > instead of that patch, please use this no-zombies version that kills the > started process group instead of pid-only. Very cool :-). The only minor annoyance for me is that my Mac pops up a message asking me if I want pgAdmin to accept connections, but there's nothing you can do about that of course. At this point I think there are a couple of things left to do; - Add more tests! - Add command line options to runtests.py to allow users to run either the existing tests or the acceptance tests (or both, which should be the default). Of course, it should still be possible to just run any single test. Thanks! -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-27 16:28 Dave Page <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-01-27 16:28 UTC (permalink / raw) To: George Gelashvili <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers On Fri, Jan 27, 2017 at 4:11 PM, Dave Page <[email protected]> wrote: > On Thu, Jan 26, 2017 at 10:40 PM, George Gelashvili > <[email protected]> wrote: >> instead of that patch, please use this no-zombies version that kills the >> started process group instead of pid-only. > > Very cool :-). The only minor annoyance for me is that my Mac pops up > a message asking me if I want pgAdmin to accept connections, but > there's nothing you can do about that of course. > > At this point I think there are a couple of things left to do; > > - Add more tests! > > - Add command line options to runtests.py to allow users to run either > the existing tests or the acceptance tests (or both, which should be > the default). Of course, it should still be possible to just run any > single test. Please add: - Proper cleanup. I just noticed the tests have left an "acceptable_test_db" database behind. Thanks. -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-30 19:28 George Gelashvili <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: George Gelashvili @ 2017-01-30 19:28 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers so, it sounds like you're saying our accaptable_test_db is unacceptable :-P here's a patch that takes an "--exclude" flag (see README) and doesn't create dbs that don't get cleaned up afterwards On Fri, Jan 27, 2017 at 11:28 AM, Dave Page <[email protected]> wrote: > On Fri, Jan 27, 2017 at 4:11 PM, Dave Page <[email protected]> wrote: > > On Thu, Jan 26, 2017 at 10:40 PM, George Gelashvili > > <[email protected]> wrote: > >> instead of that patch, please use this no-zombies version that kills the > >> started process group instead of pid-only. > > > > Very cool :-). The only minor annoyance for me is that my Mac pops up > > a message asking me if I want pgAdmin to accept connections, but > > there's nothing you can do about that of course. > > > > At this point I think there are a couple of things left to do; > > > > - Add more tests! > > > > - Add command line options to runtests.py to allow users to run either > > the existing tests or the acceptance tests (or both, which should be > > the default). Of course, it should still be possible to just run any > > single test. > > Please add: > > - Proper cleanup. I just noticed the tests have left an > "acceptable_test_db" database behind. > > Thanks. > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..47d077d5 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,72 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config as app_config +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + self.app_starter.start_app() + self.page.wait_for_app() + + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def tearDown(self): + self.app_starter.stop_app() + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py new file mode 100644 index 00000000..807dff5a --- /dev/null +++ b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py @@ -0,0 +1,68 @@ +import time +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class SQLTemplateSelectionByPostgresVersionWorksFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + self.app_starter.stop_app() diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..996892a6 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,27 +54,23 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, pkg_root, exclude_pkgs): cls.registry = dict() + all_modules = [] + + all_modules += find_modules(pkg_root, False, True) + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): - try: - if "tests." in str(module_name): - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): - try: - # Exclude the test cases in browser node if SERVER_MODE - # is False - if "pgadmin.browser.tests" not in module_name: - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) + for module_name in all_modules: + try: + if "tests." in str(module_name) and not any( + str(module_name).startswith('pgadmin.' + str(exclude_pkg)) for exclude_pkg in exclude_pkgs + ): + import_module(module_name) + except ImportError: + traceback.print_exc(file=sys.stderr) import six diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..5b077d81 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in @@ -122,3 +126,8 @@ Execution: Example 2) Run test framework for 'database' node run 'python runtests.py --pkg browser.server_groups.servers.databases' + +- Exclude a package and its subpackages when running tests: + + Example: exclude acceptance tests but run all others: + run 'python runtests.py --exclude acceptance' diff --git a/web/regression/runtests.py b/web/regression/runtests.py index 8d2a886a..cd372b4e 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -133,12 +133,20 @@ def get_test_modules(arguments): from pgadmin.utils.route import TestsGeneratorRegistry + exclude_pkgs = [] + + if not config.SERVER_MODE: + exclude_pkgs.append("browser.tests") + if 'exclude' in arguments: + exclude_pkgs.append(arguments['exclude']) + # Load the test modules which are in given package(i.e. in arguments.pkg) if arguments['pkg'] is None or arguments['pkg'] == "all": - TestsGeneratorRegistry.load_generators('pgadmin') + TestsGeneratorRegistry.load_generators('pgadmin', exclude_pkgs) else: TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % - arguments['pkg']) + arguments['pkg'], + exclude_pkgs) # Sort module list so that test suite executes the test cases sequentially module_list = TestsGeneratorRegistry.registry.items() @@ -159,6 +167,8 @@ def add_arguments(): parser = argparse.ArgumentParser(description='Test suite for pgAdmin4') parser.add_argument('--pkg', help='Executes the test cases of particular' ' package') + parser.add_argument('--exclude', help='Skips execution of the test ' + 'cases of particular package') arg = parser.parse_args() return arg diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..68f36cbc 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,24 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..fffd9526 --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,27 @@ +import os +import subprocess + +import signal + + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + print("opening browser") + self.driver.get("http://"; + self.app_config.DEFAULT_SERVER + ":" + str(self.app_config.DEFAULT_SERVER_PORT)) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..dce257c5 --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,106 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + self.find_by_xpath("//input[@name='" + field_name + "']").clear() + self.find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [text/plain] acceptance-tests-with-exclude.diff (18.3K, 3-acceptance-tests-with-exclude.diff) download | inline diff: diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..47d077d5 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,72 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config as app_config +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + self.app_starter.start_app() + self.page.wait_for_app() + + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def tearDown(self): + self.app_starter.stop_app() + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py new file mode 100644 index 00000000..807dff5a --- /dev/null +++ b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py @@ -0,0 +1,68 @@ +import time +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class SQLTemplateSelectionByPostgresVersionWorksFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + self.app_starter.stop_app() diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..996892a6 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,27 +54,23 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, pkg_root, exclude_pkgs): cls.registry = dict() + all_modules = [] + + all_modules += find_modules(pkg_root, False, True) + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): - try: - if "tests." in str(module_name): - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): - try: - # Exclude the test cases in browser node if SERVER_MODE - # is False - if "pgadmin.browser.tests" not in module_name: - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) + for module_name in all_modules: + try: + if "tests." in str(module_name) and not any( + str(module_name).startswith('pgadmin.' + str(exclude_pkg)) for exclude_pkg in exclude_pkgs + ): + import_module(module_name) + except ImportError: + traceback.print_exc(file=sys.stderr) import six diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..5b077d81 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in @@ -122,3 +126,8 @@ Execution: Example 2) Run test framework for 'database' node run 'python runtests.py --pkg browser.server_groups.servers.databases' + +- Exclude a package and its subpackages when running tests: + + Example: exclude acceptance tests but run all others: + run 'python runtests.py --exclude acceptance' diff --git a/web/regression/runtests.py b/web/regression/runtests.py index 8d2a886a..cd372b4e 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -133,12 +133,20 @@ def get_test_modules(arguments): from pgadmin.utils.route import TestsGeneratorRegistry + exclude_pkgs = [] + + if not config.SERVER_MODE: + exclude_pkgs.append("browser.tests") + if 'exclude' in arguments: + exclude_pkgs.append(arguments['exclude']) + # Load the test modules which are in given package(i.e. in arguments.pkg) if arguments['pkg'] is None or arguments['pkg'] == "all": - TestsGeneratorRegistry.load_generators('pgadmin') + TestsGeneratorRegistry.load_generators('pgadmin', exclude_pkgs) else: TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % - arguments['pkg']) + arguments['pkg'], + exclude_pkgs) # Sort module list so that test suite executes the test cases sequentially module_list = TestsGeneratorRegistry.registry.items() @@ -159,6 +167,8 @@ def add_arguments(): parser = argparse.ArgumentParser(description='Test suite for pgAdmin4') parser.add_argument('--pkg', help='Executes the test cases of particular' ' package') + parser.add_argument('--exclude', help='Skips execution of the test ' + 'cases of particular package') arg = parser.parse_args() return arg diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..68f36cbc 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,24 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..fffd9526 --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,27 @@ +import os +import subprocess + +import signal + + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + print("opening browser") + self.driver.get("http://" + self.app_config.DEFAULT_SERVER + ":" + str(self.app_config.DEFAULT_SERVER_PORT)) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..dce257c5 --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,106 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + self.find_by_xpath("//input[@name='" + field_name + "']").clear() + self.find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-30 21:23 Atira Odhner <[email protected]> parent: George Gelashvili <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Atira Odhner @ 2017-01-30 21:23 UTC (permalink / raw) To: George Gelashvili <[email protected]>; +Cc: Dave Page <[email protected]>; pgadmin-hackers Here's the patch with one more fix -- cleaning up the connections that get created in pgAdmin. On Mon, Jan 30, 2017 at 2:28 PM, George Gelashvili <[email protected]> wrote: > so, it sounds like you're saying our accaptable_test_db is unacceptable :-P > > here's a patch that takes an "--exclude" flag (see README) and doesn't > create dbs that don't get cleaned up afterwards > > On Fri, Jan 27, 2017 at 11:28 AM, Dave Page <[email protected]> wrote: > >> On Fri, Jan 27, 2017 at 4:11 PM, Dave Page <[email protected]> wrote: >> > On Thu, Jan 26, 2017 at 10:40 PM, George Gelashvili >> > <[email protected]> wrote: >> >> instead of that patch, please use this no-zombies version that kills >> the >> >> started process group instead of pid-only. >> > >> > Very cool :-). The only minor annoyance for me is that my Mac pops up >> > a message asking me if I want pgAdmin to accept connections, but >> > there's nothing you can do about that of course. >> > >> > At this point I think there are a couple of things left to do; >> > >> > - Add more tests! >> > >> > - Add command line options to runtests.py to allow users to run either >> > the existing tests or the acceptance tests (or both, which should be >> > the default). Of course, it should still be possible to just run any >> > single test. >> >> Please add: >> >> - Proper cleanup. I just noticed the tests have left an >> "acceptable_test_db" database behind. >> >> Thanks. >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> > > diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..08e92154 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,73 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config as app_config +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + self.app_starter.start_app() + self.page.wait_for_app() + + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def tearDown(self): + self.page.remove_server(self.server) + self.app_starter.stop_app() + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py new file mode 100644 index 00000000..ed6c0d6b --- /dev/null +++ b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py @@ -0,0 +1,68 @@ +import time +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class SQLTemplateSelectionByPostgresVersionWorksFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + self.app_starter.stop_app() + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..996892a6 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,27 +54,23 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, pkg_root, exclude_pkgs): cls.registry = dict() + all_modules = [] + + all_modules += find_modules(pkg_root, False, True) + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): - try: - if "tests." in str(module_name): - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): - try: - # Exclude the test cases in browser node if SERVER_MODE - # is False - if "pgadmin.browser.tests" not in module_name: - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) + for module_name in all_modules: + try: + if "tests." in str(module_name) and not any( + str(module_name).startswith('pgadmin.' + str(exclude_pkg)) for exclude_pkg in exclude_pkgs + ): + import_module(module_name) + except ImportError: + traceback.print_exc(file=sys.stderr) import six diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..5b077d81 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in @@ -122,3 +126,8 @@ Execution: Example 2) Run test framework for 'database' node run 'python runtests.py --pkg browser.server_groups.servers.databases' + +- Exclude a package and its subpackages when running tests: + + Example: exclude acceptance tests but run all others: + run 'python runtests.py --exclude acceptance' diff --git a/web/regression/runtests.py b/web/regression/runtests.py index 8d2a886a..cd372b4e 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -133,12 +133,20 @@ def get_test_modules(arguments): from pgadmin.utils.route import TestsGeneratorRegistry + exclude_pkgs = [] + + if not config.SERVER_MODE: + exclude_pkgs.append("browser.tests") + if 'exclude' in arguments: + exclude_pkgs.append(arguments['exclude']) + # Load the test modules which are in given package(i.e. in arguments.pkg) if arguments['pkg'] is None or arguments['pkg'] == "all": - TestsGeneratorRegistry.load_generators('pgadmin') + TestsGeneratorRegistry.load_generators('pgadmin', exclude_pkgs) else: TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % - arguments['pkg']) + arguments['pkg'], + exclude_pkgs) # Sort module list so that test suite executes the test cases sequentially module_list = TestsGeneratorRegistry.registry.items() @@ -159,6 +167,8 @@ def add_arguments(): parser = argparse.ArgumentParser(description='Test suite for pgAdmin4') parser.add_argument('--pkg', help='Executes the test cases of particular' ' package') + parser.add_argument('--exclude', help='Skips execution of the test ' + 'cases of particular package') arg = parser.parse_args() return arg diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..68f36cbc 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,24 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..fffd9526 --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,27 @@ +import os +import subprocess + +import signal + + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + print("opening browser") + self.driver.get("http://"; + self.app_config.DEFAULT_SERVER + ":" + str(self.app_config.DEFAULT_SERVER_PORT)) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..4e334e80 --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,106 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + self.find_by_xpath("//input[@name='" + field_name + "']").clear() + self.find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [text/plain] acceptance-tests-with-exclude-and-connection-cleanup.diff (18.4K, 3-acceptance-tests-with-exclude-and-connection-cleanup.diff) download | inline diff: diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..08e92154 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,73 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config as app_config +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + self.app_starter.start_app() + self.page.wait_for_app() + + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def tearDown(self): + self.page.remove_server(self.server) + self.app_starter.stop_app() + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_test_screenshot.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py new file mode 100644 index 00000000..ed6c0d6b --- /dev/null +++ b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py @@ -0,0 +1,68 @@ +import time +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class SQLTemplateSelectionByPostgresVersionWorksFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + self.app_starter.stop_app() + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..996892a6 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,27 +54,23 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, pkg_root, exclude_pkgs): cls.registry = dict() + all_modules = [] + + all_modules += find_modules(pkg_root, False, True) + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): - try: - if "tests." in str(module_name): - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): - try: - # Exclude the test cases in browser node if SERVER_MODE - # is False - if "pgadmin.browser.tests" not in module_name: - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) + for module_name in all_modules: + try: + if "tests." in str(module_name) and not any( + str(module_name).startswith('pgadmin.' + str(exclude_pkg)) for exclude_pkg in exclude_pkgs + ): + import_module(module_name) + except ImportError: + traceback.print_exc(file=sys.stderr) import six diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..5b077d81 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in @@ -122,3 +126,8 @@ Execution: Example 2) Run test framework for 'database' node run 'python runtests.py --pkg browser.server_groups.servers.databases' + +- Exclude a package and its subpackages when running tests: + + Example: exclude acceptance tests but run all others: + run 'python runtests.py --exclude acceptance' diff --git a/web/regression/runtests.py b/web/regression/runtests.py index 8d2a886a..cd372b4e 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -133,12 +133,20 @@ def get_test_modules(arguments): from pgadmin.utils.route import TestsGeneratorRegistry + exclude_pkgs = [] + + if not config.SERVER_MODE: + exclude_pkgs.append("browser.tests") + if 'exclude' in arguments: + exclude_pkgs.append(arguments['exclude']) + # Load the test modules which are in given package(i.e. in arguments.pkg) if arguments['pkg'] is None or arguments['pkg'] == "all": - TestsGeneratorRegistry.load_generators('pgadmin') + TestsGeneratorRegistry.load_generators('pgadmin', exclude_pkgs) else: TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % - arguments['pkg']) + arguments['pkg'], + exclude_pkgs) # Sort module list so that test suite executes the test cases sequentially module_list = TestsGeneratorRegistry.registry.items() @@ -159,6 +167,8 @@ def add_arguments(): parser = argparse.ArgumentParser(description='Test suite for pgAdmin4') parser.add_argument('--pkg', help='Executes the test cases of particular' ' package') + parser.add_argument('--exclude', help='Skips execution of the test ' + 'cases of particular package') arg = parser.parse_args() return arg diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..68f36cbc 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,24 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..fffd9526 --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,27 @@ +import os +import subprocess + +import signal + + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py"], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w')) + + print("opening browser") + self.driver.get("http://" + self.app_config.DEFAULT_SERVER + ":" + str(self.app_config.DEFAULT_SERVER_PORT)) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..4e334e80 --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,106 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + self.find_by_xpath("//input[@name='" + field_name + "']").clear() + self.find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-31 14:41 Dave Page <[email protected]> parent: Atira Odhner <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-01-31 14:41 UTC (permalink / raw) To: Atira Odhner <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers Hi On Mon, Jan 30, 2017 at 9:23 PM, Atira Odhner <[email protected]> wrote: > Here's the patch with one more fix -- cleaning up the connections that get > created in pgAdmin. Hmm, I had trouble with this one. I noticed a few issues: - The tests started pgAdmin listening on the default port (5050), however, I already had an instance running on there; a) It should have detected that something else was running on the port b) Shouldn't we just use a random, unused port? - Errors were given because I already had an acceptance_test_db on a number of servers, and that contained the test table. Obviously the code now cleans up after itself, but I think we should use a random database name as the main regression tests do (they append a random number to the name iirc). - Some of the tests just seemed to time out. I *think* this might be because the test browser window opens quite narrowly, and it looks like the tests are probably trying to do things with nodes that aren't actually visible. ====================================================================== ERROR: runTest (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py", line 69, in tearDown self.app_starter.stop_app() File "/Users/dpage/git/pgadmin4/web/regression/utils/app_starter.py", line 27, in stop_app os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) OSError: [Errno 3] No such process ====================================================================== ERROR: runTest (pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py", line 37, in runTest self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", line 45, in find_by_xpath return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", line 72, in wait_for_element return self._wait_for("element to exist", element_if_it_exists) File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", line 106, in _wait_for raise RuntimeError("timed out waiting for " + waiting_for_message) RuntimeError: timed out waiting for element to exist ====================================================================== ERROR: runTest (pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py", line 60, in tearDown self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", line 45, in find_by_xpath return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", line 72, in wait_for_element return self._wait_for("element to exist", element_if_it_exists) File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", line 106, in _wait_for raise RuntimeError("timed out waiting for " + waiting_for_message) RuntimeError: timed out waiting for element to exist ---------------------------------------------------------------------- Thanks. -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-31 14:54 George Gelashvili <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: George Gelashvili @ 2017-01-31 14:54 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers Hi Dave, We agree that a random port would be a nice addition. We think having randomized test database names can lead to polluting with lots of extra databases left around in the event that cleanup fails for whatever reason (e.g. a test errors out). We see this happen already with the randomized test databases you mention. We agree that there should probably be one strategy across the test suite. We could use randomized names and have a more general cleanup step that removes all databases of the form "test_...". Dave, are those errors you saw when you shut down your application on :5050 and did a fresh run of the tests? If not, could you please do a clean run? It's possible that the second error could be related to viewport size as you suggested, but the first error just looks like a problem with the test not being able to spin up its own server. Thanks, George & Tira On Tue, Jan 31, 2017 at 9:41 AM, Dave Page <[email protected]> wrote: > Hi > > On Mon, Jan 30, 2017 at 9:23 PM, Atira Odhner <[email protected]> wrote: > > Here's the patch with one more fix -- cleaning up the connections that > get > > created in pgAdmin. > > Hmm, I had trouble with this one. I noticed a few issues: > > - The tests started pgAdmin listening on the default port (5050), > however, I already had an instance running on there; > a) It should have detected that something else was running on the port > b) Shouldn't we just use a random, unused port? > > - Errors were given because I already had an acceptance_test_db on a > number of servers, and that contained the test table. Obviously the > code now cleans up after itself, but I think we should use a random > database name as the main regression tests do (they append a random > number to the name iirc). > > - Some of the tests just seemed to time out. I *think* this might be > because the test browser window opens quite narrowly, and it looks > like the tests are probably trying to do things with nodes that aren't > actually visible. > > ====================================================================== > ERROR: runTest (pgadmin.acceptance.tests.connect_to_server_feature_test. > ConnectsToServerFeatureTest) > ---------------------------------------------------------------------- > Traceback (most recent call last): > File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/ > connect_to_server_feature_test.py", > line 69, in tearDown > self.app_starter.stop_app() > File "/Users/dpage/git/pgadmin4/web/regression/utils/app_starter.py", > line 27, in stop_app > os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) > OSError: [Errno 3] No such process > > ====================================================================== > ERROR: runTest (pgadmin.acceptance.tests.sql_template_selection_by_ > postgres_version_works_feature_test.SQLTemplateSelectionByPostgres > VersionWorksFeatureTest) > ---------------------------------------------------------------------- > Traceback (most recent call last): > File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/ > sql_template_selection_by_postgres_version_works_feature_test.py", > line 37, in runTest > self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' > and .='Trigger Functions']").click() > File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > line 45, in find_by_xpath > return self.wait_for_element(lambda: > self.driver.find_element_by_xpath(xpath)) > File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > line 72, in wait_for_element > return self._wait_for("element to exist", element_if_it_exists) > File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > line 106, in _wait_for > raise RuntimeError("timed out waiting for " + waiting_for_message) > RuntimeError: timed out waiting for element to exist > > ====================================================================== > ERROR: runTest (pgadmin.acceptance.tests.sql_template_selection_by_ > postgres_version_works_feature_test.SQLTemplateSelectionByPostgres > VersionWorksFeatureTest) > ---------------------------------------------------------------------- > Traceback (most recent call last): > File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/ > sql_template_selection_by_postgres_version_works_feature_test.py", > line 60, in tearDown > self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() > File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > line 45, in find_by_xpath > return self.wait_for_element(lambda: > self.driver.find_element_by_xpath(xpath)) > File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > line 72, in wait_for_element > return self._wait_for("element to exist", element_if_it_exists) > File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > line 106, in _wait_for > raise RuntimeError("timed out waiting for " + waiting_for_message) > RuntimeError: timed out waiting for element to exist > > ---------------------------------------------------------------------- > > Thanks. > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-31 15:10 Dave Page <[email protected]> parent: George Gelashvili <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-01-31 15:10 UTC (permalink / raw) To: George Gelashvili <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers Hi On Tue, Jan 31, 2017 at 2:54 PM, George Gelashvili <[email protected]> wrote: > Hi Dave, > > We agree that a random port would be a nice addition. We think having > randomized test database names can lead to polluting with lots of extra > databases left around in the event that cleanup fails for whatever reason > (e.g. a test errors out). We see this happen already with the randomized > test databases you mention. We agree that there should probably be one > strategy across the test suite. We could use randomized names and have a > more general cleanup step that removes all databases of the form "test_...". I'm very wary about doing things like that. We had an early version of the suite that managed to delete all databases :-/. Maybe we could use a patterned name, but only delete databases that also have a comment with some text in it that we can verify? > Dave, are those errors you saw when you shut down your application on :5050 > and did a fresh run of the tests? If not, could you please do a clean run? > It's possible that the second error could be related to viewport size as you > suggested, but the first error just looks like a problem with the test not > being able to spin up its own server. That was on a second run of the tests, yes. I just did a careful cleanup of left-over test databases, double-checked my server wasn't running and re-ran the tests - I got the same results. > > Thanks, > George & Tira > > On Tue, Jan 31, 2017 at 9:41 AM, Dave Page <[email protected]> wrote: >> >> Hi >> >> On Mon, Jan 30, 2017 at 9:23 PM, Atira Odhner <[email protected]> wrote: >> > Here's the patch with one more fix -- cleaning up the connections that >> > get >> > created in pgAdmin. >> >> Hmm, I had trouble with this one. I noticed a few issues: >> >> - The tests started pgAdmin listening on the default port (5050), >> however, I already had an instance running on there; >> a) It should have detected that something else was running on the port >> b) Shouldn't we just use a random, unused port? >> >> - Errors were given because I already had an acceptance_test_db on a >> number of servers, and that contained the test table. Obviously the >> code now cleans up after itself, but I think we should use a random >> database name as the main regression tests do (they append a random >> number to the name iirc). >> >> - Some of the tests just seemed to time out. I *think* this might be >> because the test browser window opens quite narrowly, and it looks >> like the tests are probably trying to do things with nodes that aren't >> actually visible. >> >> ====================================================================== >> ERROR: runTest >> (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) >> ---------------------------------------------------------------------- >> Traceback (most recent call last): >> File >> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py", >> line 69, in tearDown >> self.app_starter.stop_app() >> File "/Users/dpage/git/pgadmin4/web/regression/utils/app_starter.py", >> line 27, in stop_app >> os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) >> OSError: [Errno 3] No such process >> >> ====================================================================== >> ERROR: runTest >> (pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest) >> ---------------------------------------------------------------------- >> Traceback (most recent call last): >> File >> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py", >> line 37, in runTest >> self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' >> and .='Trigger Functions']").click() >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >> line 45, in find_by_xpath >> return self.wait_for_element(lambda: >> self.driver.find_element_by_xpath(xpath)) >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >> line 72, in wait_for_element >> return self._wait_for("element to exist", element_if_it_exists) >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >> line 106, in _wait_for >> raise RuntimeError("timed out waiting for " + waiting_for_message) >> RuntimeError: timed out waiting for element to exist >> >> ====================================================================== >> ERROR: runTest >> (pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest) >> ---------------------------------------------------------------------- >> Traceback (most recent call last): >> File >> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py", >> line 60, in tearDown >> self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >> line 45, in find_by_xpath >> return self.wait_for_element(lambda: >> self.driver.find_element_by_xpath(xpath)) >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >> line 72, in wait_for_element >> return self._wait_for("element to exist", element_if_it_exists) >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >> line 106, in _wait_for >> raise RuntimeError("timed out waiting for " + waiting_for_message) >> RuntimeError: timed out waiting for element to exist >> >> ---------------------------------------------------------------------- >> >> Thanks. >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company > > -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-01-31 16:25 Dave Page <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-01-31 16:25 UTC (permalink / raw) To: George Gelashvili <[email protected]>; +Cc: Atira Odhner <[email protected]>; pgadmin-hackers Hi George, I just tried to do some debugging of pgAdmin, and found that I couldn't start it. On further investigation, I found that I had an instance running in the background on my system. I'm assuming this was started by the acceptance tests, but not shutdown. I killed it off, and re-ran the tests only to see failures because the database and table used in the acceptance tests were still present. When the tests completed, pgAdmin was again left running in the background. I've just re-run the tests, having first killed the backgrounded pgAdmin and then manually cleaned up the test objects. This time I do indeed only get the two errors below when it tests the first of 3 servers I have configured. The second and third servers get three errors each, and pgAdmin is left running in the background again. So, you were right that I had another instance of pgAdmin running... but it was tests that caused it :-p On Tue, Jan 31, 2017 at 3:10 PM, Dave Page <[email protected]> wrote: > Hi > > On Tue, Jan 31, 2017 at 2:54 PM, George Gelashvili > <[email protected]> wrote: >> Hi Dave, >> >> We agree that a random port would be a nice addition. We think having >> randomized test database names can lead to polluting with lots of extra >> databases left around in the event that cleanup fails for whatever reason >> (e.g. a test errors out). We see this happen already with the randomized >> test databases you mention. We agree that there should probably be one >> strategy across the test suite. We could use randomized names and have a >> more general cleanup step that removes all databases of the form "test_...". > > I'm very wary about doing things like that. We had an early version of > the suite that managed to delete all databases :-/. Maybe we could use > a patterned name, but only delete databases that also have a comment > with some text in it that we can verify? > >> Dave, are those errors you saw when you shut down your application on :5050 >> and did a fresh run of the tests? If not, could you please do a clean run? >> It's possible that the second error could be related to viewport size as you >> suggested, but the first error just looks like a problem with the test not >> being able to spin up its own server. > > That was on a second run of the tests, yes. I just did a careful > cleanup of left-over test databases, double-checked my server wasn't > running and re-ran the tests - I got the same results. > >> >> Thanks, >> George & Tira >> >> On Tue, Jan 31, 2017 at 9:41 AM, Dave Page <[email protected]> wrote: >>> >>> Hi >>> >>> On Mon, Jan 30, 2017 at 9:23 PM, Atira Odhner <[email protected]> wrote: >>> > Here's the patch with one more fix -- cleaning up the connections that >>> > get >>> > created in pgAdmin. >>> >>> Hmm, I had trouble with this one. I noticed a few issues: >>> >>> - The tests started pgAdmin listening on the default port (5050), >>> however, I already had an instance running on there; >>> a) It should have detected that something else was running on the port >>> b) Shouldn't we just use a random, unused port? >>> >>> - Errors were given because I already had an acceptance_test_db on a >>> number of servers, and that contained the test table. Obviously the >>> code now cleans up after itself, but I think we should use a random >>> database name as the main regression tests do (they append a random >>> number to the name iirc). >>> >>> - Some of the tests just seemed to time out. I *think* this might be >>> because the test browser window opens quite narrowly, and it looks >>> like the tests are probably trying to do things with nodes that aren't >>> actually visible. >>> >>> ====================================================================== >>> ERROR: runTest >>> (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) >>> ---------------------------------------------------------------------- >>> Traceback (most recent call last): >>> File >>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py", >>> line 69, in tearDown >>> self.app_starter.stop_app() >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/app_starter.py", >>> line 27, in stop_app >>> os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) >>> OSError: [Errno 3] No such process >>> >>> ====================================================================== >>> ERROR: runTest >>> (pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest) >>> ---------------------------------------------------------------------- >>> Traceback (most recent call last): >>> File >>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py", >>> line 37, in runTest >>> self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' >>> and .='Trigger Functions']").click() >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >>> line 45, in find_by_xpath >>> return self.wait_for_element(lambda: >>> self.driver.find_element_by_xpath(xpath)) >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >>> line 72, in wait_for_element >>> return self._wait_for("element to exist", element_if_it_exists) >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >>> line 106, in _wait_for >>> raise RuntimeError("timed out waiting for " + waiting_for_message) >>> RuntimeError: timed out waiting for element to exist >>> >>> ====================================================================== >>> ERROR: runTest >>> (pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest) >>> ---------------------------------------------------------------------- >>> Traceback (most recent call last): >>> File >>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py", >>> line 60, in tearDown >>> self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >>> line 45, in find_by_xpath >>> return self.wait_for_element(lambda: >>> self.driver.find_element_by_xpath(xpath)) >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >>> line 72, in wait_for_element >>> return self._wait_for("element to exist", element_if_it_exists) >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >>> line 106, in _wait_for >>> raise RuntimeError("timed out waiting for " + waiting_for_message) >>> RuntimeError: timed out waiting for element to exist >>> >>> ---------------------------------------------------------------------- >>> >>> Thanks. >>> >>> -- >>> Dave Page >>> Blog: http://pgsnake.blogspot.com >>> Twitter: @pgsnake >>> >>> EnterpriseDB UK: http://www.enterprisedb.com >>> The Enterprise PostgreSQL Company >> >> > > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-03 21:56 Atira Odhner <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Atira Odhner @ 2017-02-03 21:56 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers Hi Dave, Here is a new patch which includes the following: - randomized ports - delete the acceptance_test_db database in setup in case a prior run failed - fixed size browser window Cheers, Tira & George On Tue, Jan 31, 2017 at 11:25 AM, Dave Page <[email protected]> wrote: > Hi George, > > I just tried to do some debugging of pgAdmin, and found that I > couldn't start it. On further investigation, I found that I had an > instance running in the background on my system. I'm assuming this was > started by the acceptance tests, but not shutdown. I killed it off, > and re-ran the tests only to see failures because the database and > table used in the acceptance tests were still present. When the tests > completed, pgAdmin was again left running in the background. > > I've just re-run the tests, having first killed the backgrounded > pgAdmin and then manually cleaned up the test objects. This time I do > indeed only get the two errors below when it tests the first of 3 > servers I have configured. The second and third servers get three > errors each, and pgAdmin is left running in the background again. > > So, you were right that I had another instance of pgAdmin running... > but it was tests that caused it :-p > > > > On Tue, Jan 31, 2017 at 3:10 PM, Dave Page <[email protected]> wrote: > > Hi > > > > On Tue, Jan 31, 2017 at 2:54 PM, George Gelashvili > > <[email protected]> wrote: > >> Hi Dave, > >> > >> We agree that a random port would be a nice addition. We think having > >> randomized test database names can lead to polluting with lots of extra > >> databases left around in the event that cleanup fails for whatever > reason > >> (e.g. a test errors out). We see this happen already with the > randomized > >> test databases you mention. We agree that there should probably be one > >> strategy across the test suite. We could use randomized names and have a > >> more general cleanup step that removes all databases of the form > "test_...". > > > > I'm very wary about doing things like that. We had an early version of > > the suite that managed to delete all databases :-/. Maybe we could use > > a patterned name, but only delete databases that also have a comment > > with some text in it that we can verify? > > > >> Dave, are those errors you saw when you shut down your application on > :5050 > >> and did a fresh run of the tests? If not, could you please do a clean > run? > >> It's possible that the second error could be related to viewport size > as you > >> suggested, but the first error just looks like a problem with the test > not > >> being able to spin up its own server. > > > > That was on a second run of the tests, yes. I just did a careful > > cleanup of left-over test databases, double-checked my server wasn't > > running and re-ran the tests - I got the same results. > > > >> > >> Thanks, > >> George & Tira > >> > >> On Tue, Jan 31, 2017 at 9:41 AM, Dave Page <[email protected]> wrote: > >>> > >>> Hi > >>> > >>> On Mon, Jan 30, 2017 at 9:23 PM, Atira Odhner <[email protected]> > wrote: > >>> > Here's the patch with one more fix -- cleaning up the connections > that > >>> > get > >>> > created in pgAdmin. > >>> > >>> Hmm, I had trouble with this one. I noticed a few issues: > >>> > >>> - The tests started pgAdmin listening on the default port (5050), > >>> however, I already had an instance running on there; > >>> a) It should have detected that something else was running on the > port > >>> b) Shouldn't we just use a random, unused port? > >>> > >>> - Errors were given because I already had an acceptance_test_db on a > >>> number of servers, and that contained the test table. Obviously the > >>> code now cleans up after itself, but I think we should use a random > >>> database name as the main regression tests do (they append a random > >>> number to the name iirc). > >>> > >>> - Some of the tests just seemed to time out. I *think* this might be > >>> because the test browser window opens quite narrowly, and it looks > >>> like the tests are probably trying to do things with nodes that aren't > >>> actually visible. > >>> > >>> ====================================================================== > >>> ERROR: runTest > >>> (pgadmin.acceptance.tests.connect_to_server_feature_test. > ConnectsToServerFeatureTest) > >>> ---------------------------------------------------------------------- > >>> Traceback (most recent call last): > >>> File > >>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/ > connect_to_server_feature_test.py", > >>> line 69, in tearDown > >>> self.app_starter.stop_app() > >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/app_ > starter.py", > >>> line 27, in stop_app > >>> os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) > >>> OSError: [Errno 3] No such process > >>> > >>> ====================================================================== > >>> ERROR: runTest > >>> (pgadmin.acceptance.tests.sql_template_selection_by_ > postgres_version_works_feature_test.SQLTemplateSelectionByPostgres > VersionWorksFeatureTest) > >>> ---------------------------------------------------------------------- > >>> Traceback (most recent call last): > >>> File > >>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/ > sql_template_selection_by_postgres_version_works_feature_test.py", > >>> line 37, in runTest > >>> self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' > >>> and .='Trigger Functions']").click() > >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_ > page.py", > >>> line 45, in find_by_xpath > >>> return self.wait_for_element(lambda: > >>> self.driver.find_element_by_xpath(xpath)) > >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_ > page.py", > >>> line 72, in wait_for_element > >>> return self._wait_for("element to exist", element_if_it_exists) > >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_ > page.py", > >>> line 106, in _wait_for > >>> raise RuntimeError("timed out waiting for " + waiting_for_message) > >>> RuntimeError: timed out waiting for element to exist > >>> > >>> ====================================================================== > >>> ERROR: runTest > >>> (pgadmin.acceptance.tests.sql_template_selection_by_ > postgres_version_works_feature_test.SQLTemplateSelectionByPostgres > VersionWorksFeatureTest) > >>> ---------------------------------------------------------------------- > >>> Traceback (most recent call last): > >>> File > >>> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/ > sql_template_selection_by_postgres_version_works_feature_test.py", > >>> line 60, in tearDown > >>> self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() > >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_ > page.py", > >>> line 45, in find_by_xpath > >>> return self.wait_for_element(lambda: > >>> self.driver.find_element_by_xpath(xpath)) > >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_ > page.py", > >>> line 72, in wait_for_element > >>> return self._wait_for("element to exist", element_if_it_exists) > >>> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_ > page.py", > >>> line 106, in _wait_for > >>> raise RuntimeError("timed out waiting for " + waiting_for_message) > >>> RuntimeError: timed out waiting for element to exist > >>> > >>> ---------------------------------------------------------------------- > >>> > >>> Thanks. > >>> > >>> -- > >>> Dave Page > >>> Blog: http://pgsnake.blogspot.com > >>> Twitter: @pgsnake > >>> > >>> EnterpriseDB UK: http://www.enterprisedb.com > >>> The Enterprise PostgreSQL Company > >> > >> > > > > > > > > -- > > Dave Page > > Blog: http://pgsnake.blogspot.com > > Twitter: @pgsnake > > > > EnterpriseDB UK: http://www.enterprisedb.com > > The Enterprise PostgreSQL Company > > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py index 68848c00..95e675b7 100644 --- a/web/pgAdmin4.py +++ b/web/pgAdmin4.py @@ -59,6 +59,12 @@ if 'PGADMIN_PORT' in globals(): globals()['PGADMIN_PORT']) server_port = int(globals()['PGADMIN_PORT']) PGADMIN_RUNTIME = True +elif 'PGADMIN_PORT' in os.environ: + port = os.environ['PGADMIN_PORT'] + app.logger.debug( + 'Not running under the desktop runtime, port: %s', + port) + server_port = int(port) else: app.logger.debug( 'Not running under the desktop runtime, port: %s', diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..b54696f6 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,73 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config as app_config +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + self.app_starter.start_app() + self.page.wait_for_app() + + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def tearDown(self): + self.page.remove_server(self.server) + self.app_starter.stop_app() + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_connect_to_server_test_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py new file mode 100644 index 00000000..65b96eae --- /dev/null +++ b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py @@ -0,0 +1,78 @@ +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class SQLTemplateSelectionByPostgresVersionWorksFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + self.app_starter.stop_app() + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_sql_template_selection_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..996892a6 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,27 +54,23 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, pkg_root, exclude_pkgs): cls.registry = dict() + all_modules = [] + + all_modules += find_modules(pkg_root, False, True) + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): - try: - if "tests." in str(module_name): - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): - try: - # Exclude the test cases in browser node if SERVER_MODE - # is False - if "pgadmin.browser.tests" not in module_name: - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) + for module_name in all_modules: + try: + if "tests." in str(module_name) and not any( + str(module_name).startswith('pgadmin.' + str(exclude_pkg)) for exclude_pkg in exclude_pkgs + ): + import_module(module_name) + except ImportError: + traceback.print_exc(file=sys.stderr) import six diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..5b077d81 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in @@ -122,3 +126,8 @@ Execution: Example 2) Run test framework for 'database' node run 'python runtests.py --pkg browser.server_groups.servers.databases' + +- Exclude a package and its subpackages when running tests: + + Example: exclude acceptance tests but run all others: + run 'python runtests.py --exclude acceptance' diff --git a/web/regression/runtests.py b/web/regression/runtests.py index 272e3802..7b1bf543 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -138,12 +138,20 @@ def get_test_modules(arguments): from pgadmin.utils.route import TestsGeneratorRegistry + exclude_pkgs = [] + + if not config.SERVER_MODE: + exclude_pkgs.append("browser.tests") + if 'exclude' in arguments: + exclude_pkgs.append(arguments['exclude']) + # Load the test modules which are in given package(i.e. in arguments.pkg) if arguments['pkg'] is None or arguments['pkg'] == "all": - TestsGeneratorRegistry.load_generators('pgadmin') + TestsGeneratorRegistry.load_generators('pgadmin', exclude_pkgs) else: TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % - arguments['pkg']) + arguments['pkg'], + exclude_pkgs) # Sort module list so that test suite executes the test cases sequentially module_list = TestsGeneratorRegistry.registry.items() @@ -164,6 +172,8 @@ def add_arguments(): parser = argparse.ArgumentParser(description='Test suite for pgAdmin4') parser.add_argument('--pkg', help='Executes the test cases of particular' ' package') + parser.add_argument('--exclude', help='Skips execution of the test ' + 'cases of particular package') arg = parser.parse_args() return arg diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..68f36cbc 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,24 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..b297bd6d --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,34 @@ +import os +import subprocess + +import signal + +import random + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + random_server_port = str(random.randint(10000, 65535)) + env = {"PGADMIN_PORT": random_server_port} + env.update(os.environ) + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py", "magic-portal", random_server_port], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w'), + env=env) + + self.driver.set_window_size(1024, 1024) + print("opening browser") + self.driver.get("http://"; + self.app_config.DEFAULT_SERVER + ":" + random_server_port) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..4e334e80 --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,106 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + self.find_by_xpath("//input[@name='" + field_name + "']").clear() + self.find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [text/plain] acceptance_with_random_ports.diff (19.8K, 3-acceptance_with_random_ports.diff) download | inline diff: diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py index 68848c00..95e675b7 100644 --- a/web/pgAdmin4.py +++ b/web/pgAdmin4.py @@ -59,6 +59,12 @@ if 'PGADMIN_PORT' in globals(): globals()['PGADMIN_PORT']) server_port = int(globals()['PGADMIN_PORT']) PGADMIN_RUNTIME = True +elif 'PGADMIN_PORT' in os.environ: + port = os.environ['PGADMIN_PORT'] + app.logger.debug( + 'Not running under the desktop runtime, port: %s', + port) + server_port = int(port) else: app.logger.debug( 'Not running under the desktop runtime, port: %s', diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..b54696f6 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,73 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config as app_config +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + self.app_starter.start_app() + self.page.wait_for_app() + + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def tearDown(self): + self.page.remove_server(self.server) + self.app_starter.stop_app() + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_connect_to_server_test_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py new file mode 100644 index 00000000..65b96eae --- /dev/null +++ b/web/pgadmin/acceptance/tests/sql_template_selection_by_postgres_version_works_feature_test.py @@ -0,0 +1,78 @@ +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class SQLTemplateSelectionByPostgresVersionWorksFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + self.app_starter.stop_app() + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_sql_template_selection_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..996892a6 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,27 +54,23 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, pkg_root, exclude_pkgs): cls.registry = dict() + all_modules = [] + + all_modules += find_modules(pkg_root, False, True) + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): - try: - if "tests." in str(module_name): - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): - try: - # Exclude the test cases in browser node if SERVER_MODE - # is False - if "pgadmin.browser.tests" not in module_name: - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) + for module_name in all_modules: + try: + if "tests." in str(module_name) and not any( + str(module_name).startswith('pgadmin.' + str(exclude_pkg)) for exclude_pkg in exclude_pkgs + ): + import_module(module_name) + except ImportError: + traceback.print_exc(file=sys.stderr) import six diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..5b077d81 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in @@ -122,3 +126,8 @@ Execution: Example 2) Run test framework for 'database' node run 'python runtests.py --pkg browser.server_groups.servers.databases' + +- Exclude a package and its subpackages when running tests: + + Example: exclude acceptance tests but run all others: + run 'python runtests.py --exclude acceptance' diff --git a/web/regression/runtests.py b/web/regression/runtests.py index 272e3802..7b1bf543 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -138,12 +138,20 @@ def get_test_modules(arguments): from pgadmin.utils.route import TestsGeneratorRegistry + exclude_pkgs = [] + + if not config.SERVER_MODE: + exclude_pkgs.append("browser.tests") + if 'exclude' in arguments: + exclude_pkgs.append(arguments['exclude']) + # Load the test modules which are in given package(i.e. in arguments.pkg) if arguments['pkg'] is None or arguments['pkg'] == "all": - TestsGeneratorRegistry.load_generators('pgadmin') + TestsGeneratorRegistry.load_generators('pgadmin', exclude_pkgs) else: TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % - arguments['pkg']) + arguments['pkg'], + exclude_pkgs) # Sort module list so that test suite executes the test cases sequentially module_list = TestsGeneratorRegistry.registry.items() @@ -164,6 +172,8 @@ def add_arguments(): parser = argparse.ArgumentParser(description='Test suite for pgAdmin4') parser.add_argument('--pkg', help='Executes the test cases of particular' ' package') + parser.add_argument('--exclude', help='Skips execution of the test ' + 'cases of particular package') arg = parser.parse_args() return arg diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..68f36cbc 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,24 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..b297bd6d --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,34 @@ +import os +import subprocess + +import signal + +import random + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + random_server_port = str(random.randint(10000, 65535)) + env = {"PGADMIN_PORT": random_server_port} + env.update(os.environ) + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py", "magic-portal", random_server_port], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w'), + env=env) + + self.driver.set_window_size(1024, 1024) + print("opening browser") + self.driver.get("http://" + self.app_config.DEFAULT_SERVER + ":" + random_server_port) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..4e334e80 --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,106 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + self.find_by_xpath("//input[@name='" + field_name + "']").clear() + self.find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-06 10:32 Dave Page <[email protected]> parent: Atira Odhner <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-02-06 10:32 UTC (permalink / raw) To: Atira Odhner <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers Hi On Fri, Feb 3, 2017 at 9:56 PM, Atira Odhner <[email protected]> wrote: > Hi Dave, > > Here is a new patch which includes the following: > - randomized ports > - delete the acceptance_test_db database in setup in case a prior run failed > - fixed size browser window Definitely getting there :-). A couple of thoughts/questions: - Now there are 2 tests in there, it's clear that both the Python server and browser session are restarted for each test. Can this be avoided? It'll really slow down test execution as more and more are added. - We've got a new monster name: pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest (which on disk is sql_template_select_by_postgres_version_works_feature_test.py). Names like that really must be shortened to something more sane and manageable. - I'm a little confused by why the tests cannot be run in server mode. The error says it's because the username/password is unknown - however, both the pgAdmin and database server usernames and passwords are in test_config.json. Thanks! -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-06 14:54 Atira Odhner <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Atira Odhner @ 2017-02-06 14:54 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers I agree that we should rename the test. We've renamed it to "template_selection_feature_test". Your other suggestions are captured in our backlog as future improvements. We definitely can and should do those things but I think it would be valuable to go ahead and get this suite in and give other devs a chance to use and iterate on this work. Thanks, Tira & George On Mon, Feb 6, 2017 at 5:32 AM, Dave Page <[email protected]> wrote: > Hi > > On Fri, Feb 3, 2017 at 9:56 PM, Atira Odhner <[email protected]> wrote: > > Hi Dave, > > > > Here is a new patch which includes the following: > > - randomized ports > > - delete the acceptance_test_db database in setup in case a prior run > failed > > - fixed size browser window > > Definitely getting there :-). A couple of thoughts/questions: > > - Now there are 2 tests in there, it's clear that both the Python > server and browser session are restarted for each test. Can this be > avoided? It'll really slow down test execution as more and more are > added. > > - We've got a new monster name: > pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_ > feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest > (which on disk is > sql_template_select_by_postgres_version_works_feature_test.py). Names > like that really must be shortened to something more sane and > manageable. > > - I'm a little confused by why the tests cannot be run in server mode. > The error says it's because the username/password is unknown - > however, both the pgAdmin and database server usernames and passwords > are in test_config.json. > > Thanks! > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py index 68848c00..95e675b7 100644 --- a/web/pgAdmin4.py +++ b/web/pgAdmin4.py @@ -59,6 +59,12 @@ if 'PGADMIN_PORT' in globals(): globals()['PGADMIN_PORT']) server_port = int(globals()['PGADMIN_PORT']) PGADMIN_RUNTIME = True +elif 'PGADMIN_PORT' in os.environ: + port = os.environ['PGADMIN_PORT'] + app.logger.debug( + 'Not running under the desktop runtime, port: %s', + port) + server_port = int(port) else: app.logger.debug( 'Not running under the desktop runtime, port: %s', diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..b54696f6 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,73 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config as app_config +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + self.app_starter.start_app() + self.page.wait_for_app() + + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def tearDown(self): + self.page.remove_server(self.server) + self.app_starter.stop_app() + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_connect_to_server_test_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/acceptance/tests/template_selection_feature_test.py b/web/pgadmin/acceptance/tests/template_selection_feature_test.py new file mode 100644 index 00000000..b7405d56 --- /dev/null +++ b/web/pgadmin/acceptance/tests/template_selection_feature_test.py @@ -0,0 +1,78 @@ +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class TemplateSelectionFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + self.app_starter.stop_app() + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_sql_template_selection_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..996892a6 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,27 +54,23 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, pkg_root, exclude_pkgs): cls.registry = dict() + all_modules = [] + + all_modules += find_modules(pkg_root, False, True) + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): - try: - if "tests." in str(module_name): - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): - try: - # Exclude the test cases in browser node if SERVER_MODE - # is False - if "pgadmin.browser.tests" not in module_name: - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) + for module_name in all_modules: + try: + if "tests." in str(module_name) and not any( + str(module_name).startswith('pgadmin.' + str(exclude_pkg)) for exclude_pkg in exclude_pkgs + ): + import_module(module_name) + except ImportError: + traceback.print_exc(file=sys.stderr) import six diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..5b077d81 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in @@ -122,3 +126,8 @@ Execution: Example 2) Run test framework for 'database' node run 'python runtests.py --pkg browser.server_groups.servers.databases' + +- Exclude a package and its subpackages when running tests: + + Example: exclude acceptance tests but run all others: + run 'python runtests.py --exclude acceptance' diff --git a/web/regression/runtests.py b/web/regression/runtests.py index 272e3802..7b1bf543 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -138,12 +138,20 @@ def get_test_modules(arguments): from pgadmin.utils.route import TestsGeneratorRegistry + exclude_pkgs = [] + + if not config.SERVER_MODE: + exclude_pkgs.append("browser.tests") + if 'exclude' in arguments: + exclude_pkgs.append(arguments['exclude']) + # Load the test modules which are in given package(i.e. in arguments.pkg) if arguments['pkg'] is None or arguments['pkg'] == "all": - TestsGeneratorRegistry.load_generators('pgadmin') + TestsGeneratorRegistry.load_generators('pgadmin', exclude_pkgs) else: TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % - arguments['pkg']) + arguments['pkg'], + exclude_pkgs) # Sort module list so that test suite executes the test cases sequentially module_list = TestsGeneratorRegistry.registry.items() @@ -164,6 +172,8 @@ def add_arguments(): parser = argparse.ArgumentParser(description='Test suite for pgAdmin4') parser.add_argument('--pkg', help='Executes the test cases of particular' ' package') + parser.add_argument('--exclude', help='Skips execution of the test ' + 'cases of particular package') arg = parser.parse_args() return arg diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..68f36cbc 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,24 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..b297bd6d --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,34 @@ +import os +import subprocess + +import signal + +import random + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + random_server_port = str(random.randint(10000, 65535)) + env = {"PGADMIN_PORT": random_server_port} + env.update(os.environ) + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py", "magic-portal", random_server_port], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w'), + env=env) + + self.driver.set_window_size(1024, 1024) + print("opening browser") + self.driver.get("http://"; + self.app_config.DEFAULT_SERVER + ":" + random_server_port) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..4e334e80 --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,106 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + self.find_by_xpath("//input[@name='" + field_name + "']").clear() + self.find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [text/plain] acceptance_test_with_randomized_ports_renamed.diff (19.7K, 3-acceptance_test_with_randomized_ports_renamed.diff) download | inline diff: diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py index 68848c00..95e675b7 100644 --- a/web/pgAdmin4.py +++ b/web/pgAdmin4.py @@ -59,6 +59,12 @@ if 'PGADMIN_PORT' in globals(): globals()['PGADMIN_PORT']) server_port = int(globals()['PGADMIN_PORT']) PGADMIN_RUNTIME = True +elif 'PGADMIN_PORT' in os.environ: + port = os.environ['PGADMIN_PORT'] + app.logger.debug( + 'Not running under the desktop runtime, port: %s', + port) + server_port = int(port) else: app.logger.debug( 'Not running under the desktop runtime, port: %s', diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..b54696f6 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,73 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +import time + +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + +from pgadmin.utils.route import BaseTestGenerator + +import subprocess +import os +import signal +import config as app_config +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + self.app_starter.start_app() + self.page.wait_for_app() + + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + self.page.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def tearDown(self): + self.page.remove_server(self.server) + self.app_starter.stop_app() + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_connect_to_server_test_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/acceptance/tests/template_selection_feature_test.py b/web/pgadmin/acceptance/tests/template_selection_feature_test.py new file mode 100644 index 00000000..b7405d56 --- /dev/null +++ b/web/pgadmin/acceptance/tests/template_selection_feature_test.py @@ -0,0 +1,78 @@ +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class TemplateSelectionFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + self.app_starter.stop_app() + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_sql_template_selection_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index f18d2c18..996892a6 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -54,27 +54,23 @@ class TestsGeneratorRegistry(ABCMeta): ABCMeta.__init__(cls, name, bases, d) @classmethod - def load_generators(cls, pkg): + def load_generators(cls, pkg_root, exclude_pkgs): cls.registry = dict() + all_modules = [] + + all_modules += find_modules(pkg_root, False, True) + # Check for SERVER mode - if config.SERVER_MODE: - for module_name in find_modules(pkg, False, True): - try: - if "tests." in str(module_name): - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) - else: - for module_name in find_modules(pkg, False, True): - try: - # Exclude the test cases in browser node if SERVER_MODE - # is False - if "pgadmin.browser.tests" not in module_name: - import_module(module_name) - except ImportError: - traceback.print_exc(file=sys.stderr) + for module_name in all_modules: + try: + if "tests." in str(module_name) and not any( + str(module_name).startswith('pgadmin.' + str(exclude_pkg)) for exclude_pkg in exclude_pkgs + ): + import_module(module_name) + except ImportError: + traceback.print_exc(file=sys.stderr) import six diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..5b077d81 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in @@ -122,3 +126,8 @@ Execution: Example 2) Run test framework for 'database' node run 'python runtests.py --pkg browser.server_groups.servers.databases' + +- Exclude a package and its subpackages when running tests: + + Example: exclude acceptance tests but run all others: + run 'python runtests.py --exclude acceptance' diff --git a/web/regression/runtests.py b/web/regression/runtests.py index 272e3802..7b1bf543 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -138,12 +138,20 @@ def get_test_modules(arguments): from pgadmin.utils.route import TestsGeneratorRegistry + exclude_pkgs = [] + + if not config.SERVER_MODE: + exclude_pkgs.append("browser.tests") + if 'exclude' in arguments: + exclude_pkgs.append(arguments['exclude']) + # Load the test modules which are in given package(i.e. in arguments.pkg) if arguments['pkg'] is None or arguments['pkg'] == "all": - TestsGeneratorRegistry.load_generators('pgadmin') + TestsGeneratorRegistry.load_generators('pgadmin', exclude_pkgs) else: TestsGeneratorRegistry.load_generators('pgadmin.%s.tests' % - arguments['pkg']) + arguments['pkg'], + exclude_pkgs) # Sort module list so that test suite executes the test cases sequentially module_list = TestsGeneratorRegistry.registry.items() @@ -164,6 +172,8 @@ def add_arguments(): parser = argparse.ArgumentParser(description='Test suite for pgAdmin4') parser.add_argument('--pkg', help='Executes the test cases of particular' ' package') + parser.add_argument('--exclude', help='Skips execution of the test ' + 'cases of particular package') arg = parser.parse_args() return arg diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..68f36cbc 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,24 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..b297bd6d --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,34 @@ +import os +import subprocess + +import signal + +import random + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + random_server_port = str(random.randint(10000, 65535)) + env = {"PGADMIN_PORT": random_server_port} + env.update(os.environ) + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py", "magic-portal", random_server_port], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w'), + env=env) + + self.driver.set_window_size(1024, 1024) + print("opening browser") + self.driver.get("http://" + self.app_config.DEFAULT_SERVER + ":" + random_server_port) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..4e334e80 --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,106 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + self.find_by_xpath("//input[@name='" + field_name + "']").clear() + self.find_by_xpath("//input[@name='" + field_name + "']").send_keys( + field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 5 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-08 22:15 Atira Odhner <[email protected]> parent: Atira Odhner <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Atira Odhner @ 2017-02-08 22:15 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers Hey Dave, We re-used one of the test helpers for the 'fix-greenplum-show-tables.diff' patch, so here is an updated patch which does not include adding that test helper in case you apply the show-tables patch first. Also, we saw some strange test behavior yesterday where form fields weren't being filled in correctly so we changed the way that input fields get filled to be more reliable. In short these need to be applied in this order: > git apply fix-greenplum-show-tables.diff git apply acceptance-tests-minus-create-table-helper-with-fixed-inputs.diff We also moved the --exclude flag changes out to a separate patch. On our side we are still dealing with these as 20 separate commits. What is the best way for us to send you these patches? Do you prefer having them all squashed down to a single patch or to have smaller patches? On Mon, Feb 6, 2017 at 9:54 AM, Atira Odhner <[email protected]> wrote: > I agree that we should rename the test. We've renamed it to > "template_selection_feature_test". > Your other suggestions are captured in our backlog as future improvements. > We definitely can and should do those things but I think it would be > valuable to go ahead and get this suite in and give other devs a chance to > use and iterate on this work. > > Thanks, > > Tira & George > > On Mon, Feb 6, 2017 at 5:32 AM, Dave Page <[email protected]> wrote: > >> Hi >> >> On Fri, Feb 3, 2017 at 9:56 PM, Atira Odhner <[email protected]> wrote: >> > Hi Dave, >> > >> > Here is a new patch which includes the following: >> > - randomized ports >> > - delete the acceptance_test_db database in setup in case a prior run >> failed >> > - fixed size browser window >> >> Definitely getting there :-). A couple of thoughts/questions: >> >> - Now there are 2 tests in there, it's clear that both the Python >> server and browser session are restarted for each test. Can this be >> avoided? It'll really slow down test execution as more and more are >> added. >> >> - We've got a new monster name: >> pgadmin.acceptance.tests.sql_template_selection_by_postgres_ >> version_works_feature_test.SQLTemplateSelectionByPostgresVer >> sionWorksFeatureTest >> (which on disk is >> sql_template_select_by_postgres_version_works_feature_test.py). Names >> like that really must be shortened to something more sane and >> manageable. >> >> - I'm a little confused by why the tests cannot be run in server mode. >> The error says it's because the username/password is unknown - >> however, both the pgAdmin and database server usernames and passwords >> are in test_config.json. >> >> Thanks! >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> > > commit f340178c3f2d779ea5368f48a36f47112e2fa316 Author: George Gelashvili and Tira Odhner <[email protected]> Date: Wed Feb 8 14:52:32 2017 -0500 ACCEPTANCE TEST SQUASH Based on Work around input validation by sending backspace to clear diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py index 68848c00..95e675b7 100644 --- a/web/pgAdmin4.py +++ b/web/pgAdmin4.py @@ -59,6 +59,12 @@ if 'PGADMIN_PORT' in globals(): globals()['PGADMIN_PORT']) server_port = int(globals()['PGADMIN_PORT']) PGADMIN_RUNTIME = True +elif 'PGADMIN_PORT' in os.environ: + port = os.environ['PGADMIN_PORT'] + app.logger.debug( + 'Not running under the desktop runtime, port: %s', + port) + server_port = int(port) else: app.logger.debug( 'Not running under the desktop runtime, port: %s', diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..c3bee4e6 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,92 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + test_utils.create_database(self.server, "acceptance_test_db") + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.app_starter.start_app() + self.page.wait_for_app() + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self._connects_to_server() + self._tables_node_expandable() + + def tearDown(self): + self.page.remove_server(self.server) + self.app_starter.stop_app() + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_connect_to_server_test_failure.png') + return AssertionError(*args, **kwargs) + + def _connects_to_server(self): + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + def _tables_node_expandable(self): + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.toggle_open_tree_item('Tables') + self.page.toggle_open_tree_item('test_table') diff --git a/web/pgadmin/acceptance/tests/template_selection_feature_test.py b/web/pgadmin/acceptance/tests/template_selection_feature_test.py new file mode 100644 index 00000000..b7405d56 --- /dev/null +++ b/web/pgadmin/acceptance/tests/template_selection_feature_test.py @@ -0,0 +1,78 @@ +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class TemplateSelectionFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + self.app_starter.stop_app() + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_sql_template_selection_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..ae5d268d 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..b297bd6d --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,34 @@ +import os +import subprocess + +import signal + +import random + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + random_server_port = str(random.randint(10000, 65535)) + env = {"PGADMIN_PORT": random_server_port} + env.update(os.environ) + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py", "magic-portal", random_server_port], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w'), + env=env) + + self.driver.set_window_size(1024, 1024) + print("opening browser") + self.driver.get("http://"; + self.app_config.DEFAULT_SERVER + ":" + random_server_port) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..d6a5836c --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,120 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains +from selenium.webdriver.common.keys import Keys + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + field = self.find_by_xpath("//input[@name='" + field_name + "']") + backspaces = [Keys.BACKSPACE]*len(field.get_attribute('value')) + + field.click() + field.send_keys(backspaces) + field.send_keys(str(field_content)) + self.wait_for_input_field_content(field_name, field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_input_field_content(self, field_name, content): + def input_field_has_content(): + element = self.driver.find_element_by_xpath( + "//input[@name='" + field_name + "']") + + return str(content) == element.get_attribute('value') + + return self._wait_for("field to contain '" + str(content) + "'", input_field_has_content) + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 10 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [text/plain] acceptance-tests-minus-create-table-helper-with-fixed-inputs.diff (17.1K, 3-acceptance-tests-minus-create-table-helper-with-fixed-inputs.diff) download | inline diff: commit f340178c3f2d779ea5368f48a36f47112e2fa316 Author: George Gelashvili and Tira Odhner <[email protected]> Date: Wed Feb 8 14:52:32 2017 -0500 ACCEPTANCE TEST SQUASH Based on Work around input validation by sending backspace to clear diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py index 68848c00..95e675b7 100644 --- a/web/pgAdmin4.py +++ b/web/pgAdmin4.py @@ -59,6 +59,12 @@ if 'PGADMIN_PORT' in globals(): globals()['PGADMIN_PORT']) server_port = int(globals()['PGADMIN_PORT']) PGADMIN_RUNTIME = True +elif 'PGADMIN_PORT' in os.environ: + port = os.environ['PGADMIN_PORT'] + app.logger.debug( + 'Not running under the desktop runtime, port: %s', + port) + server_port = int(port) else: app.logger.debug( 'Not running under the desktop runtime, port: %s', diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..c3bee4e6 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,92 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + test_utils.create_database(self.server, "acceptance_test_db") + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.app_starter.start_app() + self.page.wait_for_app() + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self._connects_to_server() + self._tables_node_expandable() + + def tearDown(self): + self.page.remove_server(self.server) + self.app_starter.stop_app() + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_connect_to_server_test_failure.png') + return AssertionError(*args, **kwargs) + + def _connects_to_server(self): + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + def _tables_node_expandable(self): + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.toggle_open_tree_item('Tables') + self.page.toggle_open_tree_item('test_table') diff --git a/web/pgadmin/acceptance/tests/template_selection_feature_test.py b/web/pgadmin/acceptance/tests/template_selection_feature_test.py new file mode 100644 index 00000000..b7405d56 --- /dev/null +++ b/web/pgadmin/acceptance/tests/template_selection_feature_test.py @@ -0,0 +1,78 @@ +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class TemplateSelectionFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + self.app_starter.stop_app() + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_sql_template_selection_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..ae5d268d 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..b297bd6d --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,34 @@ +import os +import subprocess + +import signal + +import random + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + random_server_port = str(random.randint(10000, 65535)) + env = {"PGADMIN_PORT": random_server_port} + env.update(os.environ) + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py", "magic-portal", random_server_port], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w'), + env=env) + + self.driver.set_window_size(1024, 1024) + print("opening browser") + self.driver.get("http://" + self.app_config.DEFAULT_SERVER + ":" + random_server_port) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..d6a5836c --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,120 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains +from selenium.webdriver.common.keys import Keys + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + field = self.find_by_xpath("//input[@name='" + field_name + "']") + backspaces = [Keys.BACKSPACE]*len(field.get_attribute('value')) + + field.click() + field.send_keys(backspaces) + field.send_keys(str(field_content)) + self.wait_for_input_field_content(field_name, field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_input_field_content(self, field_name, content): + def input_field_has_content(): + element = self.driver.find_element_by_xpath( + "//input[@name='" + field_name + "']") + + return str(content) == element.get_attribute('value') + + return self._wait_for("field to contain '" + str(content) + "'", input_field_has_content) + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 10 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-09 12:47 Dave Page <[email protected]> parent: Atira Odhner <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-02-09 12:47 UTC (permalink / raw) To: Atira Odhner <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers Hi I get the following crash when running with Python 3.4 or 3.5: (pgadmin4-py34) piranha:pgadmin4 dpage$ python web/regression/runtests.py pgAdmin 4 - Application Initialisation ====================================== The configuration database - '/Users/dpage/.pgadmin/test_pgadmin4.db' does not exist. Entering initial setup mode... NOTE: Configuring authentication for DESKTOP mode. The configuration database has been created at /Users/dpage/.pgadmin/test_pgadmin4.db =============Running the test cases for 'Regression - PG 9.4'============= runTest (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) ... Traceback (most recent call last): File "web/regression/runtests.py", line 276, in <module> verbosity=2).run(suite) File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/runner.py", line 168, in run test(result) File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py", line 84, in __call__ return self.run(*args, **kwds) File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py", line 122, in run test(result) File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", line 628, in __call__ return self.run(*args, **kwds) File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", line 588, in run self._feedErrorsToResult(result, outcome.errors) File "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", line 515, in _feedErrorsToResult if issubclass(exc_info[0], self.failureException): TypeError: issubclass() arg 2 must be a class or tuple of classes With Python 2.7, it initially opens Chrome with the URL "data:," (without the quotes), and then spits out: ====================================================================== ERROR: runTest (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py", line 41, in setUp test_utils.create_table(self.server, "acceptance_test_db", "test_table") AttributeError: 'module' object has no attribute 'create_table' ====================================================================== ERROR: runTest (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py", line 36, in runTest test_utils.create_table(self.server, "acceptance_test_db", "test_table") AttributeError: 'module' object has no attribute 'create_table' ====================================================================== ERROR: runTest (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py", line 66, in tearDown self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", line 46, in find_by_xpath return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", line 86, in wait_for_element return self._wait_for("element to exist", element_if_it_exists) File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", line 120, in _wait_for raise RuntimeError("timed out waiting for " + waiting_for_message) RuntimeError: timed out waiting for element to exist ---------------------------------------------------------------------- Ran 149 tests in 59.258s FAILED (errors=3, skipped=12) On Wed, Feb 8, 2017 at 10:15 PM, Atira Odhner <[email protected]> wrote: > Hey Dave, > > We re-used one of the test helpers for the 'fix-greenplum-show-tables.diff' > patch, so here is an updated patch which does not include adding that test > helper in case you apply the show-tables patch first. Also, we saw some > strange test behavior yesterday where form fields weren't being filled in > correctly so we changed the way that input fields get filled to be more > reliable. > > In short these need to be applied in this order: >> >> git apply fix-greenplum-show-tables.diff >> >> git apply >> acceptance-tests-minus-create-table-helper-with-fixed-inputs.diff > > > We also moved the --exclude flag changes out to a separate patch. > > On our side we are still dealing with these as 20 separate commits. What is > the best way for us to send you these patches? Do you prefer having them all > squashed down to a single patch or to have smaller patches? > > > > On Mon, Feb 6, 2017 at 9:54 AM, Atira Odhner <[email protected]> wrote: >> >> I agree that we should rename the test. We've renamed it to >> "template_selection_feature_test". >> Your other suggestions are captured in our backlog as future improvements. >> We definitely can and should do those things but I think it would be >> valuable to go ahead and get this suite in and give other devs a chance to >> use and iterate on this work. >> >> Thanks, >> >> Tira & George >> >> On Mon, Feb 6, 2017 at 5:32 AM, Dave Page <[email protected]> wrote: >>> >>> Hi >>> >>> On Fri, Feb 3, 2017 at 9:56 PM, Atira Odhner <[email protected]> wrote: >>> > Hi Dave, >>> > >>> > Here is a new patch which includes the following: >>> > - randomized ports >>> > - delete the acceptance_test_db database in setup in case a prior run >>> > failed >>> > - fixed size browser window >>> >>> Definitely getting there :-). A couple of thoughts/questions: >>> >>> - Now there are 2 tests in there, it's clear that both the Python >>> server and browser session are restarted for each test. Can this be >>> avoided? It'll really slow down test execution as more and more are >>> added. >>> >>> - We've got a new monster name: >>> >>> pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest >>> (which on disk is >>> sql_template_select_by_postgres_version_works_feature_test.py). Names >>> like that really must be shortened to something more sane and >>> manageable. >>> >>> - I'm a little confused by why the tests cannot be run in server mode. >>> The error says it's because the username/password is unknown - >>> however, both the pgAdmin and database server usernames and passwords >>> are in test_config.json. >>> >>> Thanks! >>> >>> -- >>> Dave Page >>> Blog: http://pgsnake.blogspot.com >>> Twitter: @pgsnake >>> >>> EnterpriseDB UK: http://www.enterprisedb.com >>> The Enterprise PostgreSQL Company >> >> > -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-09 13:15 Atira Odhner <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Atira Odhner @ 2017-02-09 13:15 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers create_table is the change we pulled into the other patch which would need to be applied first. On Thu, Feb 9, 2017, 7:47 AM Dave Page <[email protected]> wrote: > Hi > > I get the following crash when running with Python 3.4 or 3.5: > > (pgadmin4-py34) piranha:pgadmin4 dpage$ python web/regression/runtests.py > pgAdmin 4 - Application Initialisation > ====================================== > > > The configuration database - '/Users/dpage/.pgadmin/test_pgadmin4.db' > does not exist. > Entering initial setup mode... > NOTE: Configuring authentication for DESKTOP mode. > > The configuration database has been created at > /Users/dpage/.pgadmin/test_pgadmin4.db > > =============Running the test cases for 'Regression - PG 9.4'============= > runTest > (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) > ... Traceback (most recent call last): > File "web/regression/runtests.py", line 276, in <module> > verbosity=2).run(suite) > File > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/runner.py", > line 168, in run > test(result) > File > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py", > line 84, in __call__ > return self.run(*args, **kwds) > File > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py", > line 122, in run > test(result) > File > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", > line 628, in __call__ > return self.run(*args, **kwds) > File > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", > line 588, in run > self._feedErrorsToResult(result, outcome.errors) > File > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", > line 515, in _feedErrorsToResult > if issubclass(exc_info[0], self.failureException): > TypeError: issubclass() arg 2 must be a class or tuple of classes > > With Python 2.7, it initially opens Chrome with the URL "data:," > (without the quotes), and then spits out: > > ====================================================================== > ERROR: runTest > (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) > ---------------------------------------------------------------------- > Traceback (most recent call last): > File > "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py", > line 41, in setUp > test_utils.create_table(self.server, "acceptance_test_db", > "test_table") > AttributeError: 'module' object has no attribute 'create_table' > > ====================================================================== > ERROR: runTest > (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest) > ---------------------------------------------------------------------- > Traceback (most recent call last): > File > "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py", > line 36, in runTest > test_utils.create_table(self.server, "acceptance_test_db", > "test_table") > AttributeError: 'module' object has no attribute 'create_table' > > ====================================================================== > ERROR: runTest > (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest) > ---------------------------------------------------------------------- > Traceback (most recent call last): > File > "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py", > line 66, in tearDown > self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() > File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > line 46, in find_by_xpath > return self.wait_for_element(lambda: > self.driver.find_element_by_xpath(xpath)) > File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > line 86, in wait_for_element > return self._wait_for("element to exist", element_if_it_exists) > File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > line 120, in _wait_for > raise RuntimeError("timed out waiting for " + waiting_for_message) > RuntimeError: timed out waiting for element to exist > > ---------------------------------------------------------------------- > Ran 149 tests in 59.258s > > FAILED (errors=3, skipped=12) > > > On Wed, Feb 8, 2017 at 10:15 PM, Atira Odhner <[email protected]> wrote: > > Hey Dave, > > > > We re-used one of the test helpers for the > 'fix-greenplum-show-tables.diff' > > patch, so here is an updated patch which does not include adding that > test > > helper in case you apply the show-tables patch first. Also, we saw some > > strange test behavior yesterday where form fields weren't being filled in > > correctly so we changed the way that input fields get filled to be more > > reliable. > > > > In short these need to be applied in this order: > >> > >> git apply fix-greenplum-show-tables.diff > >> > >> git apply > >> acceptance-tests-minus-create-table-helper-with-fixed-inputs.diff > > > > > > We also moved the --exclude flag changes out to a separate patch. > > > > On our side we are still dealing with these as 20 separate commits. What > is > > the best way for us to send you these patches? Do you prefer having them > all > > squashed down to a single patch or to have smaller patches? > > > > > > > > On Mon, Feb 6, 2017 at 9:54 AM, Atira Odhner <[email protected]> wrote: > >> > >> I agree that we should rename the test. We've renamed it to > >> "template_selection_feature_test". > >> Your other suggestions are captured in our backlog as future > improvements. > >> We definitely can and should do those things but I think it would be > >> valuable to go ahead and get this suite in and give other devs a chance > to > >> use and iterate on this work. > >> > >> Thanks, > >> > >> Tira & George > >> > >> On Mon, Feb 6, 2017 at 5:32 AM, Dave Page <[email protected]> wrote: > >>> > >>> Hi > >>> > >>> On Fri, Feb 3, 2017 at 9:56 PM, Atira Odhner <[email protected]> > wrote: > >>> > Hi Dave, > >>> > > >>> > Here is a new patch which includes the following: > >>> > - randomized ports > >>> > - delete the acceptance_test_db database in setup in case a prior run > >>> > failed > >>> > - fixed size browser window > >>> > >>> Definitely getting there :-). A couple of thoughts/questions: > >>> > >>> - Now there are 2 tests in there, it's clear that both the Python > >>> server and browser session are restarted for each test. Can this be > >>> avoided? It'll really slow down test execution as more and more are > >>> added. > >>> > >>> - We've got a new monster name: > >>> > >>> > pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest > >>> (which on disk is > >>> sql_template_select_by_postgres_version_works_feature_test.py). Names > >>> like that really must be shortened to something more sane and > >>> manageable. > >>> > >>> - I'm a little confused by why the tests cannot be run in server mode. > >>> The error says it's because the username/password is unknown - > >>> however, both the pgAdmin and database server usernames and passwords > >>> are in test_config.json. > >>> > >>> Thanks! > >>> > >>> -- > >>> Dave Page > >>> Blog: http://pgsnake.blogspot.com > >>> Twitter: @pgsnake > >>> > >>> EnterpriseDB UK: http://www.enterprisedb.com > >>> The Enterprise PostgreSQL Company > >> > >> > > > > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-09 13:26 Dave Page <[email protected]> parent: Atira Odhner <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-02-09 13:26 UTC (permalink / raw) To: Atira Odhner <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers OK, well that one was sent back with feedback as well, so please resubmit when the relevant updates have been made to either and they've been retested. Given the amount of work you're doing at the moment, it would be helpful if you could note when one patch is dependent on another. It's hard to keep track when you're this productive! Thanks. On Thu, Feb 9, 2017 at 1:15 PM, Atira Odhner <[email protected]> wrote: > create_table is the change we pulled into the other patch which would need > to be applied first. > > > On Thu, Feb 9, 2017, 7:47 AM Dave Page <[email protected]> wrote: >> >> Hi >> >> I get the following crash when running with Python 3.4 or 3.5: >> >> (pgadmin4-py34) piranha:pgadmin4 dpage$ python web/regression/runtests.py >> pgAdmin 4 - Application Initialisation >> ====================================== >> >> >> The configuration database - '/Users/dpage/.pgadmin/test_pgadmin4.db' >> does not exist. >> Entering initial setup mode... >> NOTE: Configuring authentication for DESKTOP mode. >> >> The configuration database has been created at >> /Users/dpage/.pgadmin/test_pgadmin4.db >> >> =============Running the test cases for 'Regression - PG 9.4'============= >> runTest >> (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) >> ... Traceback (most recent call last): >> File "web/regression/runtests.py", line 276, in <module> >> verbosity=2).run(suite) >> File >> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/runner.py", >> line 168, in run >> test(result) >> File >> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py", >> line 84, in __call__ >> return self.run(*args, **kwds) >> File >> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py", >> line 122, in run >> test(result) >> File >> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", >> line 628, in __call__ >> return self.run(*args, **kwds) >> File >> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", >> line 588, in run >> self._feedErrorsToResult(result, outcome.errors) >> File >> "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", >> line 515, in _feedErrorsToResult >> if issubclass(exc_info[0], self.failureException): >> TypeError: issubclass() arg 2 must be a class or tuple of classes >> >> With Python 2.7, it initially opens Chrome with the URL "data:," >> (without the quotes), and then spits out: >> >> ====================================================================== >> ERROR: runTest >> (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) >> ---------------------------------------------------------------------- >> Traceback (most recent call last): >> File >> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py", >> line 41, in setUp >> test_utils.create_table(self.server, "acceptance_test_db", >> "test_table") >> AttributeError: 'module' object has no attribute 'create_table' >> >> ====================================================================== >> ERROR: runTest >> (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest) >> ---------------------------------------------------------------------- >> Traceback (most recent call last): >> File >> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py", >> line 36, in runTest >> test_utils.create_table(self.server, "acceptance_test_db", >> "test_table") >> AttributeError: 'module' object has no attribute 'create_table' >> >> ====================================================================== >> ERROR: runTest >> (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest) >> ---------------------------------------------------------------------- >> Traceback (most recent call last): >> File >> "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py", >> line 66, in tearDown >> self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >> line 46, in find_by_xpath >> return self.wait_for_element(lambda: >> self.driver.find_element_by_xpath(xpath)) >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >> line 86, in wait_for_element >> return self._wait_for("element to exist", element_if_it_exists) >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", >> line 120, in _wait_for >> raise RuntimeError("timed out waiting for " + waiting_for_message) >> RuntimeError: timed out waiting for element to exist >> >> ---------------------------------------------------------------------- >> Ran 149 tests in 59.258s >> >> FAILED (errors=3, skipped=12) >> >> >> On Wed, Feb 8, 2017 at 10:15 PM, Atira Odhner <[email protected]> wrote: >> > Hey Dave, >> > >> > We re-used one of the test helpers for the >> > 'fix-greenplum-show-tables.diff' >> > patch, so here is an updated patch which does not include adding that >> > test >> > helper in case you apply the show-tables patch first. Also, we saw some >> > strange test behavior yesterday where form fields weren't being filled >> > in >> > correctly so we changed the way that input fields get filled to be more >> > reliable. >> > >> > In short these need to be applied in this order: >> >> >> >> git apply fix-greenplum-show-tables.diff >> >> >> >> git apply >> >> acceptance-tests-minus-create-table-helper-with-fixed-inputs.diff >> > >> > >> > We also moved the --exclude flag changes out to a separate patch. >> > >> > On our side we are still dealing with these as 20 separate commits. What >> > is >> > the best way for us to send you these patches? Do you prefer having them >> > all >> > squashed down to a single patch or to have smaller patches? >> > >> > >> > >> > On Mon, Feb 6, 2017 at 9:54 AM, Atira Odhner <[email protected]> wrote: >> >> >> >> I agree that we should rename the test. We've renamed it to >> >> "template_selection_feature_test". >> >> Your other suggestions are captured in our backlog as future >> >> improvements. >> >> We definitely can and should do those things but I think it would be >> >> valuable to go ahead and get this suite in and give other devs a chance >> >> to >> >> use and iterate on this work. >> >> >> >> Thanks, >> >> >> >> Tira & George >> >> >> >> On Mon, Feb 6, 2017 at 5:32 AM, Dave Page <[email protected]> wrote: >> >>> >> >>> Hi >> >>> >> >>> On Fri, Feb 3, 2017 at 9:56 PM, Atira Odhner <[email protected]> >> >>> wrote: >> >>> > Hi Dave, >> >>> > >> >>> > Here is a new patch which includes the following: >> >>> > - randomized ports >> >>> > - delete the acceptance_test_db database in setup in case a prior >> >>> > run >> >>> > failed >> >>> > - fixed size browser window >> >>> >> >>> Definitely getting there :-). A couple of thoughts/questions: >> >>> >> >>> - Now there are 2 tests in there, it's clear that both the Python >> >>> server and browser session are restarted for each test. Can this be >> >>> avoided? It'll really slow down test execution as more and more are >> >>> added. >> >>> >> >>> - We've got a new monster name: >> >>> >> >>> >> >>> pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest >> >>> (which on disk is >> >>> sql_template_select_by_postgres_version_works_feature_test.py). Names >> >>> like that really must be shortened to something more sane and >> >>> manageable. >> >>> >> >>> - I'm a little confused by why the tests cannot be run in server mode. >> >>> The error says it's because the username/password is unknown - >> >>> however, both the pgAdmin and database server usernames and passwords >> >>> are in test_config.json. >> >>> >> >>> Thanks! >> >>> >> >>> -- >> >>> Dave Page >> >>> Blog: http://pgsnake.blogspot.com >> >>> Twitter: @pgsnake >> >>> >> >>> EnterpriseDB UK: http://www.enterprisedb.com >> >>> The Enterprise PostgreSQL Company >> >> >> >> >> > >> >> >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-09 14:20 Atira Odhner <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Atira Odhner @ 2017-02-09 14:20 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers Certainly. We did mention the dependency in the email. Would it be better to mention it in the patch name? Is there a better way for us to manage these changes? On other open source projects, I've seen github mirrors set up so that changes can be pulled in like branches rather then as patch applies. That would have avoided this situation since the parent commit would be pulled in with the same SHA from either pull request branch and git would not see it as a conflict. I'm rather new to dealing with patch files like this so I would love some tips. Thanks, Tira On Thu, Feb 9, 2017, 8:27 AM Dave Page <[email protected]> wrote: > OK, well that one was sent back with feedback as well, so please > resubmit when the relevant updates have been made to either and > they've been retested. Given the amount of work you're doing at the > moment, it would be helpful if you could note when one patch is > dependent on another. It's hard to keep track when you're this > productive! > > Thanks. > > On Thu, Feb 9, 2017 at 1:15 PM, Atira Odhner <[email protected]> wrote: > > create_table is the change we pulled into the other patch which would > need > > to be applied first. > > > > > > On Thu, Feb 9, 2017, 7:47 AM Dave Page <[email protected]> wrote: > >> > >> Hi > >> > >> I get the following crash when running with Python 3.4 or 3.5: > >> > >> (pgadmin4-py34) piranha:pgadmin4 dpage$ python > web/regression/runtests.py > >> pgAdmin 4 - Application Initialisation > >> ====================================== > >> > >> > >> The configuration database - '/Users/dpage/.pgadmin/test_pgadmin4.db' > >> does not exist. > >> Entering initial setup mode... > >> NOTE: Configuring authentication for DESKTOP mode. > >> > >> The configuration database has been created at > >> /Users/dpage/.pgadmin/test_pgadmin4.db > >> > >> =============Running the test cases for 'Regression - PG > 9.4'============= > >> runTest > >> > (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) > >> ... Traceback (most recent call last): > >> File "web/regression/runtests.py", line 276, in <module> > >> verbosity=2).run(suite) > >> File > >> > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/runner.py", > >> line 168, in run > >> test(result) > >> File > >> > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py", > >> line 84, in __call__ > >> return self.run(*args, **kwds) > >> File > >> > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/suite.py", > >> line 122, in run > >> test(result) > >> File > >> > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", > >> line 628, in __call__ > >> return self.run(*args, **kwds) > >> File > >> > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", > >> line 588, in run > >> self._feedErrorsToResult(result, outcome.errors) > >> File > >> > "/opt/local/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/unittest/case.py", > >> line 515, in _feedErrorsToResult > >> if issubclass(exc_info[0], self.failureException): > >> TypeError: issubclass() arg 2 must be a class or tuple of classes > >> > >> With Python 2.7, it initially opens Chrome with the URL "data:," > >> (without the quotes), and then spits out: > >> > >> ====================================================================== > >> ERROR: runTest > >> > (pgadmin.acceptance.tests.connect_to_server_feature_test.ConnectsToServerFeatureTest) > >> ---------------------------------------------------------------------- > >> Traceback (most recent call last): > >> File > >> > "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py", > >> line 41, in setUp > >> test_utils.create_table(self.server, "acceptance_test_db", > >> "test_table") > >> AttributeError: 'module' object has no attribute 'create_table' > >> > >> ====================================================================== > >> ERROR: runTest > >> > (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest) > >> ---------------------------------------------------------------------- > >> Traceback (most recent call last): > >> File > >> > "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py", > >> line 36, in runTest > >> test_utils.create_table(self.server, "acceptance_test_db", > >> "test_table") > >> AttributeError: 'module' object has no attribute 'create_table' > >> > >> ====================================================================== > >> ERROR: runTest > >> > (pgadmin.acceptance.tests.template_selection_feature_test.TemplateSelectionFeatureTest) > >> ---------------------------------------------------------------------- > >> Traceback (most recent call last): > >> File > >> > "/Users/dpage/git/pgadmin4/web/pgadmin/acceptance/tests/template_selection_feature_test.py", > >> line 66, in tearDown > >> self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() > >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > >> line 46, in find_by_xpath > >> return self.wait_for_element(lambda: > >> self.driver.find_element_by_xpath(xpath)) > >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > >> line 86, in wait_for_element > >> return self._wait_for("element to exist", element_if_it_exists) > >> File "/Users/dpage/git/pgadmin4/web/regression/utils/pgadmin_page.py", > >> line 120, in _wait_for > >> raise RuntimeError("timed out waiting for " + waiting_for_message) > >> RuntimeError: timed out waiting for element to exist > >> > >> ---------------------------------------------------------------------- > >> Ran 149 tests in 59.258s > >> > >> FAILED (errors=3, skipped=12) > >> > >> > >> On Wed, Feb 8, 2017 at 10:15 PM, Atira Odhner <[email protected]> > wrote: > >> > Hey Dave, > >> > > >> > We re-used one of the test helpers for the > >> > 'fix-greenplum-show-tables.diff' > >> > patch, so here is an updated patch which does not include adding that > >> > test > >> > helper in case you apply the show-tables patch first. Also, we saw > some > >> > strange test behavior yesterday where form fields weren't being filled > >> > in > >> > correctly so we changed the way that input fields get filled to be > more > >> > reliable. > >> > > >> > In short these need to be applied in this order: > >> >> > >> >> git apply fix-greenplum-show-tables.diff > >> >> > >> >> git apply > >> >> acceptance-tests-minus-create-table-helper-with-fixed-inputs.diff > >> > > >> > > >> > We also moved the --exclude flag changes out to a separate patch. > >> > > >> > On our side we are still dealing with these as 20 separate commits. > What > >> > is > >> > the best way for us to send you these patches? Do you prefer having > them > >> > all > >> > squashed down to a single patch or to have smaller patches? > >> > > >> > > >> > > >> > On Mon, Feb 6, 2017 at 9:54 AM, Atira Odhner <[email protected]> > wrote: > >> >> > >> >> I agree that we should rename the test. We've renamed it to > >> >> "template_selection_feature_test". > >> >> Your other suggestions are captured in our backlog as future > >> >> improvements. > >> >> We definitely can and should do those things but I think it would be > >> >> valuable to go ahead and get this suite in and give other devs a > chance > >> >> to > >> >> use and iterate on this work. > >> >> > >> >> Thanks, > >> >> > >> >> Tira & George > >> >> > >> >> On Mon, Feb 6, 2017 at 5:32 AM, Dave Page <[email protected]> wrote: > >> >>> > >> >>> Hi > >> >>> > >> >>> On Fri, Feb 3, 2017 at 9:56 PM, Atira Odhner <[email protected]> > >> >>> wrote: > >> >>> > Hi Dave, > >> >>> > > >> >>> > Here is a new patch which includes the following: > >> >>> > - randomized ports > >> >>> > - delete the acceptance_test_db database in setup in case a prior > >> >>> > run > >> >>> > failed > >> >>> > - fixed size browser window > >> >>> > >> >>> Definitely getting there :-). A couple of thoughts/questions: > >> >>> > >> >>> - Now there are 2 tests in there, it's clear that both the Python > >> >>> server and browser session are restarted for each test. Can this be > >> >>> avoided? It'll really slow down test execution as more and more are > >> >>> added. > >> >>> > >> >>> - We've got a new monster name: > >> >>> > >> >>> > >> >>> > pgadmin.acceptance.tests.sql_template_selection_by_postgres_version_works_feature_test.SQLTemplateSelectionByPostgresVersionWorksFeatureTest > >> >>> (which on disk is > >> >>> sql_template_select_by_postgres_version_works_feature_test.py). > Names > >> >>> like that really must be shortened to something more sane and > >> >>> manageable. > >> >>> > >> >>> - I'm a little confused by why the tests cannot be run in server > mode. > >> >>> The error says it's because the username/password is unknown - > >> >>> however, both the pgAdmin and database server usernames and > passwords > >> >>> are in test_config.json. > >> >>> > >> >>> Thanks! > >> >>> > >> >>> -- > >> >>> Dave Page > >> >>> Blog: http://pgsnake.blogspot.com > >> >>> Twitter: @pgsnake > >> >>> > >> >>> EnterpriseDB UK: http://www.enterprisedb.com > >> >>> The Enterprise PostgreSQL Company > >> >> > >> >> > >> > > >> > >> > >> > >> -- > >> Dave Page > >> Blog: http://pgsnake.blogspot.com > >> Twitter: @pgsnake > >> > >> EnterpriseDB UK: http://www.enterprisedb.com > >> The Enterprise PostgreSQL Company > > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-09 14:28 Dave Page <[email protected]> parent: Atira Odhner <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-02-09 14:28 UTC (permalink / raw) To: Atira Odhner <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers Hi On Thu, Feb 9, 2017 at 2:20 PM, Atira Odhner <[email protected]> wrote: > Certainly. We did mention the dependency in the email. Would it be better > to mention it in the patch name? I think the problem was that the way you phrased it, it sounded optional ("an updated patch which does not include adding that test helper in case you apply the show-tables patch first"). I think a clear "This patch is dependent on patch Foo" would suffice. > Is there a better way for us to manage > these changes? On other open source projects, I've seen github mirrors set > up so that changes can be pulled in like branches rather then as patch > applies. That would have avoided this situation since the parent commit > would be pulled in with the same SHA from either pull request branch and git > would not see it as a conflict. > > I'm rather new to dealing with patch files like this so I would love some > tips. The Postgres project in general is quite conservative and stuck in it's ways about how things are done (which is usually a good thing considering you trust your data to the resulting code). We're used to dealing with larger patchsets via the mailing list - typically as long as you're clear about any dependencies, it shouldn't be a problem. Some of us use tools like PyCharms for handling patches and helping with reviews etc. which I guess replaces most, if not all of the GitHub functionality over plain git. -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-09 16:17 Atira Odhner <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Atira Odhner @ 2017-02-09 16:17 UTC (permalink / raw) To: Dave Page <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers Hi Dave, I think the problem was that the way you phrased it, You're right, we totally messed that up. We were talking about making 3 patches and ended up making only 2 and forgot to reword that bit. Sorry about that. Here are the two patches for this change that resolves the AttributeError you were seeing. The first patch is identical to the patch of the same name in the other email thread. We're used to > dealing with larger patchsets via the mailing list - typically as long > as you're clear about any dependencies, it shouldn't be a problem. > Great! We'll try sending patchsets from now on and hopefully that resolves some of the issues we were seeing. Tira & George On Thu, Feb 9, 2017 at 9:28 AM, Dave Page <[email protected]> wrote: > Hi > > On Thu, Feb 9, 2017 at 2:20 PM, Atira Odhner <[email protected]> wrote: > > Certainly. We did mention the dependency in the email. Would it be > better > > to mention it in the patch name? > > I think the problem was that the way you phrased it, it sounded > optional ("an updated patch which does not include adding that test > helper in case you apply the show-tables patch first"). I think a > clear "This patch is dependent on patch Foo" would suffice. > > > Is there a better way for us to manage > > these changes? On other open source projects, I've seen github mirrors > set > > up so that changes can be pulled in like branches rather then as patch > > applies. That would have avoided this situation since the parent commit > > would be pulled in with the same SHA from either pull request branch and > git > > would not see it as a conflict. > > > > I'm rather new to dealing with patch files like this so I would love some > > tips. > > The Postgres project in general is quite conservative and stuck in > it's ways about how things are done (which is usually a good thing > considering you trust your data to the resulting code). We're used to > dealing with larger patchsets via the mailing list - typically as long > as you're clear about any dependencies, it shouldn't be a problem. > Some of us use tools like PyCharms for handling patches and helping > with reviews etc. which I guess replaces most, if not all of the > GitHub functionality over plain git. > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [application/octet-stream] 0001-Add-create_table-to-test_utils.patch (1.6K, 3-0001-Add-create_table-to-test_utils.patch) download | inline diff: From 75980f10b4cfe7cbabe62893a6ccf3ccc26949ff Mon Sep 17 00:00:00 2001 From: George Gelashvili and Tira Odhner <[email protected]> Date: Wed, 8 Feb 2017 09:45:49 -0500 Subject: [PATCH 1/3] Add create_table to test_utils --- web/regression/test_utils.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index 1f9f0522..2dbf47bb 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -134,6 +134,25 @@ def create_database(server, db_name): traceback.print_exc(file=sys.stderr) +def create_table(server, db_name, table_name): + try: + connection = get_db_connection(db_name, + server['username'], + server['db_password'], + server['host'], + server['port']) + old_isolation_level = connection.isolation_level + connection.set_isolation_level(0) + pg_cursor = connection.cursor() + pg_cursor.execute('''CREATE TABLE "%s" (name VARCHAR, value NUMERIC)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + connection.set_isolation_level(old_isolation_level) + connection.commit() + + except Exception: + traceback.print_exc(file=sys.stderr) + + def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: -- 2.11.0 [application/octet-stream] 0002-Acceptance-tests-with-input-fix.patch (17.1K, 4-0002-Acceptance-tests-with-input-fix.patch) download | inline diff: commit f340178c3f2d779ea5368f48a36f47112e2fa316 Author: George Gelashvili and Tira Odhner <[email protected]> Date: Wed Feb 8 14:52:32 2017 -0500 ACCEPTANCE TEST SQUASH Based on Work around input validation by sending backspace to clear diff --git a/requirements_py2.txt b/requirements_py2.txt index 4fb05891..998cdabf 100644 --- a/requirements_py2.txt +++ b/requirements_py2.txt @@ -36,6 +36,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/requirements_py3.txt b/requirements_py3.txt index c4490f52..2239de63 100644 --- a/requirements_py3.txt +++ b/requirements_py3.txt @@ -35,6 +35,7 @@ testscenarios==0.5.0 testtools==2.0.0 traceback2==1.4.0 unittest2==1.1.0 +selenium==3.0.2 Werkzeug==0.9.6 WTForms==2.0.2 sqlparse==0.1.19 diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py index 68848c00..95e675b7 100644 --- a/web/pgAdmin4.py +++ b/web/pgAdmin4.py @@ -59,6 +59,12 @@ if 'PGADMIN_PORT' in globals(): globals()['PGADMIN_PORT']) server_port = int(globals()['PGADMIN_PORT']) PGADMIN_RUNTIME = True +elif 'PGADMIN_PORT' in os.environ: + port = os.environ['PGADMIN_PORT'] + app.logger.debug( + 'Not running under the desktop runtime, port: %s', + port) + server_port = int(port) else: app.logger.debug( 'Not running under the desktop runtime, port: %s', diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/acceptance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/pgadmin/acceptance/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py new file mode 100644 index 00000000..c3bee4e6 --- /dev/null +++ b/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py @@ -0,0 +1,92 @@ +############################################################# +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +############################################################## + +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class ConnectsToServerFeatureTest(BaseTestGenerator): + """ + Tests that a database connection can be created from the UI + """ + + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + test_utils.create_database(self.server, "acceptance_test_db") + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.app_starter.start_app() + self.page.wait_for_app() + + def runTest(self): + self.assertEqual(app_config.APP_NAME, self.page.driver.title) + self.page.wait_for_spinner_to_disappear() + + self._connects_to_server() + self._tables_node_expandable() + + def tearDown(self): + self.page.remove_server(self.server) + self.app_starter.stop_app() + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_connect_to_server_test_failure.png') + return AssertionError(*args, **kwargs) + + def _connects_to_server(self): + self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() + self.page.driver.find_element_by_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Server...").click() + + server_config = self.server + self.page.fill_input_by_field_name("name", server_config['name']) + self.page.find_by_partial_link_text("Connection").click() + self.page.fill_input_by_field_name("host", server_config['host']) + self.page.fill_input_by_field_name("port", server_config['port']) + self.page.fill_input_by_field_name("username", server_config['username']) + self.page.fill_input_by_field_name("password", server_config['db_password']) + self.page.find_by_xpath("//button[contains(.,'Save')]").click() + + def _tables_node_expandable(self): + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.toggle_open_tree_item('Tables') + self.page.toggle_open_tree_item('test_table') diff --git a/web/pgadmin/acceptance/tests/template_selection_feature_test.py b/web/pgadmin/acceptance/tests/template_selection_feature_test.py new file mode 100644 index 00000000..b7405d56 --- /dev/null +++ b/web/pgadmin/acceptance/tests/template_selection_feature_test.py @@ -0,0 +1,78 @@ +from selenium import webdriver +from selenium.webdriver import ActionChains + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression import test_utils +from regression.utils.app_starter import AppStarter +from regression.utils.pgadmin_page import PgadminPage + + +class TemplateSelectionFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + test_utils.create_database(self.server, "acceptance_test_db") + + self.app_starter.start_app() + self.page.wait_for_app() + + self.page.add_server(self.server) + + def runTest(self): + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + self.page.toggle_open_tree_item('Schemas') + self.page.toggle_open_tree_item('public') + self.page.find_by_xpath("//*[@id='tree']//*[@class='aciTreeText' and .='Trigger Functions']").click() + self.page.find_by_partial_link_text("Object").click() + ActionChains(self.page.driver) \ + .move_to_element(self.page.driver.find_element_by_link_text("Create")) \ + .perform() + self.page.find_by_partial_link_text("Trigger function...").click() + self.page.fill_input_by_field_name("name", "test-trigger-function") + self.page.find_by_partial_link_text("Definition").click() + self.page.fill_codemirror_area_with( +"""CREATE OR REPLACE FUNCTION log_last_name_changes() +RETURNS TRIGGER AS +$BODY$ +BEGIN + +END; +$BODY$ +""" + ) + self.page.find_by_partial_link_text("SQL").click() + + self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") + + def tearDown(self): + self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() + self.page.remove_server(self.server) + self.app_starter.stop_app() + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/pgadmin_sql_template_selection_failure.png') + return AssertionError(*args, **kwargs) diff --git a/web/regression/.gitignore b/web/regression/.gitignore index 0581810b..723fce7e 100644 --- a/web/regression/.gitignore +++ b/web/regression/.gitignore @@ -1,4 +1,5 @@ parent_id.pkl regression.log +test_greenplum_config.json test_advanced_config.json test_config.json diff --git a/web/regression/README b/web/regression/README index 8cc29987..ae5d268d 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,6 +103,10 @@ Test Data Details Execution: ----------- +- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: + get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager + and make sure it is on the PATH + - The test framework is modular and pluggable and dynamically locates tests for modules which are discovered at runtime. All test cases are found and registered automatically by its module name in diff --git a/web/regression/utils/app_starter.py b/web/regression/utils/app_starter.py new file mode 100644 index 00000000..b297bd6d --- /dev/null +++ b/web/regression/utils/app_starter.py @@ -0,0 +1,34 @@ +import os +import subprocess + +import signal + +import random + +class AppStarter: + """ + Helper for starting the full pgadmin4 app and loading the page via selenium + """ + + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def start_app(self): + random_server_port = str(random.randint(10000, 65535)) + env = {"PGADMIN_PORT": random_server_port} + env.update(os.environ) + + self.pgadmin_process = subprocess.Popen(["python", "pgAdmin4.py", "magic-portal", random_server_port], + shell=False, + preexec_fn=os.setsid, + stderr=open(os.devnull, 'w'), + env=env) + + self.driver.set_window_size(1024, 1024) + print("opening browser") + self.driver.get("http://" + self.app_config.DEFAULT_SERVER + ":" + random_server_port) + + def stop_app(self): + self.driver.close() + os.killpg(os.getpgid(self.pgadmin_process.pid), signal.SIGTERM) diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/utils/pgadmin_page.py new file mode 100644 index 00000000..d6a5836c --- /dev/null +++ b/web/regression/utils/pgadmin_page.py @@ -0,0 +1,120 @@ +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver import ActionChains +from selenium.webdriver.common.keys import Keys + + +class PgadminPage: + """ + Helper class for interacting with the page, given a selenium driver + """ + def __init__(self, driver, app_config): + self.driver = driver + self.app_config = app_config + + def add_server(self, server_config): + self.wait_for_spinner_to_disappear() + + self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() + self.driver.find_element_by_link_text("Object").click() + ActionChains(self.driver) \ + .move_to_element(self.driver.find_element_by_link_text("Create")) \ + .perform() + self.find_by_partial_link_text("Server...").click() + + self.fill_input_by_field_name("name", server_config['name']) + self.find_by_partial_link_text("Connection").click() + self.fill_input_by_field_name("host", server_config['host']) + self.fill_input_by_field_name("port", server_config['port']) + self.fill_input_by_field_name("username", server_config['username']) + self.fill_input_by_field_name("password", server_config['db_password']) + self.find_by_xpath("//button[contains(.,'Save')]").click() + + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + + def remove_server(self, server_config): + self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() + self.find_by_partial_link_text("Object").click() + self.find_by_partial_link_text("Delete/Drop").click() + time.sleep(0.5) + self.find_by_xpath("//button[contains(.,'OK')]").click() + + def toggle_open_tree_item(self, tree_item_text): + self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() + + def find_by_xpath(self, xpath): + return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) + + def find_by_id(self, element_id): + return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) + + def find_by_partial_link_text(self, link_text): + return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) + + def fill_input_by_field_name(self, field_name, field_content): + field = self.find_by_xpath("//input[@name='" + field_name + "']") + backspaces = [Keys.BACKSPACE]*len(field.get_attribute('value')) + + field.click() + field.send_keys(backspaces) + field.send_keys(str(field_content)) + self.wait_for_input_field_content(field_name, field_content) + + def fill_codemirror_area_with(self, field_content): + self.find_by_xpath( + "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() + ActionChains(self.driver).send_keys(field_content).perform() + + def wait_for_input_field_content(self, field_name, content): + def input_field_has_content(): + element = self.driver.find_element_by_xpath( + "//input[@name='" + field_name + "']") + + return str(content) == element.get_attribute('value') + + return self._wait_for("field to contain '" + str(content) + "'", input_field_has_content) + + def wait_for_element(self, find_method_with_args): + def element_if_it_exists(): + try: + element = find_method_with_args() + if element.is_displayed() & element.is_enabled(): + return element + except NoSuchElementException: + return False + + return self._wait_for("element to exist", element_if_it_exists) + + def wait_for_spinner_to_disappear(self): + def spinner_has_disappeared(): + try: + self.driver.find_element_by_id("pg-spinner") + return False + except NoSuchElementException: + return True + + self._wait_for("spinner to disappear", spinner_has_disappeared) + + def wait_for_app(self): + def page_shows_app(): + if self.driver.title == self.app_config.APP_NAME: + return True + else: + self.driver.refresh() + return False + + self._wait_for("app to start", page_shows_app) + + def _wait_for(self, waiting_for_message, condition_met_function): + timeout = 10 + time_waited = 0 + sleep_time = 0.01 + + while time_waited < timeout: + result = condition_met_function() + if result: + return result + time_waited += sleep_time + time.sleep(sleep_time) + + raise RuntimeError("timed out waiting for " + waiting_for_message) ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-13 14:36 Dave Page <[email protected]> parent: Atira Odhner <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Dave Page @ 2017-02-13 14:36 UTC (permalink / raw) To: Atira Odhner <[email protected]>; +Cc: George Gelashvili <[email protected]>; pgadmin-hackers Hi, I've been playing with this for the last couple of hours, and I just can't get it to work reliably; - A good percentage of the time the browser opens with a URL of "data:," and does nothing more. This appears to happen if tests fail, which still leaves server processes running in the background. - The connect_to_server test usually seems to work. - The template_selection_feature test usually does *not* work. I can't see an obvious reason, but I suspect it's a race condition. What seems to happen is that the function definition is entered, but not registered by the UI, so the mSQL panel just ends up saying "incomplete definition". Manually checking what was input proves that everything is correct - and indeed, returning the SQL tab shows the expected SQL. Other issues I noted: - The template_selection_feature test should just enter BEGIN/END. What it currently enters is an entire function definition, when only the body content is expected. E.g. self.page.fill_codemirror_area_with( """BEGIN END; """ ) - Screenshots are being taken of failed tests: 1) I've never actually seen any get saved 2) They should be saved to the same directory as the test log, not /tmp 3) They should have guaranteed unique names, and be mentioned in the test output so the user can reference the image to the failure. The reason the last two items are important is that I've now got a test server running the test suite with every supported version of Python, for every supported database (well, almost, pending a couple of fixes). I have separate workspaces for each Python version, and a single test run might run every test 10 times, once for each database server. - Please wrap the README at < 80 chars. On Thu, Feb 9, 2017 at 4:17 PM, Atira Odhner <[email protected]> wrote: > Hi Dave, > >> I think the problem was that the way you phrased it, > > > You're right, we totally messed that up. We were talking about making 3 > patches and ended up making only 2 and forgot to reword that bit. > Sorry about that. > > Here are the two patches for this change that resolves the AttributeError > you were seeing. The first patch is identical to the patch of the same name > in the other email thread. > >> We're used to >> dealing with larger patchsets via the mailing list - typically as long >> as you're clear about any dependencies, it shouldn't be a problem. > > > Great! We'll try sending patchsets from now on and hopefully that resolves > some of the issues we were seeing. > > Tira & George > > On Thu, Feb 9, 2017 at 9:28 AM, Dave Page <[email protected]> wrote: >> >> Hi >> >> On Thu, Feb 9, 2017 at 2:20 PM, Atira Odhner <[email protected]> wrote: >> > Certainly. We did mention the dependency in the email. Would it be >> > better >> > to mention it in the patch name? >> >> I think the problem was that the way you phrased it, it sounded >> optional ("an updated patch which does not include adding that test >> helper in case you apply the show-tables patch first"). I think a >> clear "This patch is dependent on patch Foo" would suffice. >> >> > Is there a better way for us to manage >> > these changes? On other open source projects, I've seen github mirrors >> > set >> > up so that changes can be pulled in like branches rather then as patch >> > applies. That would have avoided this situation since the parent commit >> > would be pulled in with the same SHA from either pull request branch and >> > git >> > would not see it as a conflict. >> > >> > I'm rather new to dealing with patch files like this so I would love >> > some >> > tips. >> >> The Postgres project in general is quite conservative and stuck in >> it's ways about how things are done (which is usually a good thing >> considering you trust your data to the resulting code). We're used to >> dealing with larger patchsets via the mailing list - typically as long >> as you're clear about any dependencies, it shouldn't be a problem. >> Some of us use tools like PyCharms for handling patches and helping >> with reviews etc. which I guess replaces most, if not all of the >> GitHub functionality over plain git. >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company > > -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-21 22:12 Atira Odhner <[email protected]> parent: Dave Page <[email protected]> 0 siblings, 1 reply; 30+ messages in thread From: Atira Odhner @ 2017-02-21 22:12 UTC (permalink / raw) To: Dave Page <[email protected]>; pgadmin-hackers; +Cc: Sarah McAlear <[email protected]> Hi Dave, We fixed the flakiness issues that we saw (hopefully they are the same ones you were seeing.) by tearing down connections to the acceptance_test_db before attempting to drop it at the beginning of the test. Once we have access to the CI pipeline we can help out there to ensure the flakiness is gone. We wrapped the README at 80 characters, and removed the misleading function definition from the test. As far as the screenshots go, I'm more inclined to remove the screenshotting than to work on improving it. It currently only works when the failure is due to an AssertionError since that's what failureException relies on. We also renamed acceptance to feature_tests since 'acceptance' seemed ambiguous/redundant with 'regression'. Tira & Sara On Mon, Feb 13, 2017 at 9:36 AM, Dave Page <[email protected]> wrote: > Hi, > > I've been playing with this for the last couple of hours, and I just > can't get it to work reliably; > > - A good percentage of the time the browser opens with a URL of > "data:," and does nothing more. This appears to happen if tests fail, > which still leaves server processes running in the background. > > - The connect_to_server test usually seems to work. > > - The template_selection_feature test usually does *not* work. I can't > see an obvious reason, but I suspect it's a race condition. What seems > to happen is that the function definition is entered, but not > registered by the UI, so the mSQL panel just ends up saying > "incomplete definition". Manually checking what was input proves that > everything is correct - and indeed, returning the SQL tab shows the > expected SQL. > > Other issues I noted: > > - The template_selection_feature test should just enter BEGIN/END. > What it currently enters is an entire function definition, when only > the body content is expected. E.g. > > self.page.fill_codemirror_area_with( > """BEGIN > > END; > """ > ) > > - Screenshots are being taken of failed tests: > 1) I've never actually seen any get saved > 2) They should be saved to the same directory as the test log, not /tmp > 3) They should have guaranteed unique names, and be mentioned in the > test output so the user can reference the image to the failure. > > The reason the last two items are important is that I've now got a > test server running the test suite with every supported version of > Python, for every supported database (well, almost, pending a couple > of fixes). I have separate workspaces for each Python version, and a > single test run might run every test 10 times, once for each database > server. > > - Please wrap the README at < 80 chars. > > > > On Thu, Feb 9, 2017 at 4:17 PM, Atira Odhner <[email protected]> wrote: > > Hi Dave, > > > >> I think the problem was that the way you phrased it, > > > > > > You're right, we totally messed that up. We were talking about making 3 > > patches and ended up making only 2 and forgot to reword that bit. > > Sorry about that. > > > > Here are the two patches for this change that resolves the AttributeError > > you were seeing. The first patch is identical to the patch of the same > name > > in the other email thread. > > > >> We're used to > >> dealing with larger patchsets via the mailing list - typically as long > >> as you're clear about any dependencies, it shouldn't be a problem. > > > > > > Great! We'll try sending patchsets from now on and hopefully that > resolves > > some of the issues we were seeing. > > > > Tira & George > > > > On Thu, Feb 9, 2017 at 9:28 AM, Dave Page <[email protected]> wrote: > >> > >> Hi > >> > >> On Thu, Feb 9, 2017 at 2:20 PM, Atira Odhner <[email protected]> > wrote: > >> > Certainly. We did mention the dependency in the email. Would it be > >> > better > >> > to mention it in the patch name? > >> > >> I think the problem was that the way you phrased it, it sounded > >> optional ("an updated patch which does not include adding that test > >> helper in case you apply the show-tables patch first"). I think a > >> clear "This patch is dependent on patch Foo" would suffice. > >> > >> > Is there a better way for us to manage > >> > these changes? On other open source projects, I've seen github mirrors > >> > set > >> > up so that changes can be pulled in like branches rather then as patch > >> > applies. That would have avoided this situation since the parent > commit > >> > would be pulled in with the same SHA from either pull request branch > and > >> > git > >> > would not see it as a conflict. > >> > > >> > I'm rather new to dealing with patch files like this so I would love > >> > some > >> > tips. > >> > >> The Postgres project in general is quite conservative and stuck in > >> it's ways about how things are done (which is usually a good thing > >> considering you trust your data to the resulting code). We're used to > >> dealing with larger patchsets via the mailing list - typically as long > >> as you're clear about any dependencies, it shouldn't be a problem. > >> Some of us use tools like PyCharms for handling patches and helping > >> with reviews etc. which I guess replaces most, if not all of the > >> GitHub functionality over plain git. > >> > >> -- > >> Dave Page > >> Blog: http://pgsnake.blogspot.com > >> Twitter: @pgsnake > >> > >> EnterpriseDB UK: http://www.enterprisedb.com > >> The Enterprise PostgreSQL Company > > > > > > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [application/octet-stream] 0003-Rename-acceptance-feature_tests-and-make-tests-less-.patch (12.5K, 3-0003-Rename-acceptance-feature_tests-and-make-tests-less-.patch) download | inline diff: From 52bc460dffa6497a2074178d74aafeeeae0be292 Mon Sep 17 00:00:00 2001 From: "George Gelashvili, Sarah McAlear and Tira Odhner" <[email protected]> Date: Tue, 21 Feb 2017 11:25:36 -0500 Subject: [PATCH 1/2] Rename acceptance -> feature_tests and make tests less flaky by tearing down the database connection before running the test --- .../{acceptance => feature_tests}/__init__.py | 0 .../connect_to_server_feature_test.py | 22 +++--------------- .../template_selection_feature_test.py | 24 ++++---------------- web/pgadmin/utils/route.py | 2 +- web/regression/README | 6 ++--- .../tests => regression/feature_utils}/__init__.py | 0 .../{utils => feature_utils}/app_starter.py | 0 web/regression/feature_utils/base_feature_test.py | 26 ++++++++++++++++++++++ .../{utils => feature_utils}/pgadmin_page.py | 6 ++++- web/regression/test_utils.py | 5 +++++ web/regression/utils/__init__.py | 0 11 files changed, 47 insertions(+), 44 deletions(-) rename web/pgadmin/{acceptance => feature_tests}/__init__.py (100%) rename web/pgadmin/{acceptance/tests => feature_tests}/connect_to_server_feature_test.py (78%) rename web/pgadmin/{acceptance/tests => feature_tests}/template_selection_feature_test.py (74%) rename web/{pgadmin/acceptance/tests => regression/feature_utils}/__init__.py (100%) rename web/regression/{utils => feature_utils}/app_starter.py (100%) create mode 100644 web/regression/feature_utils/base_feature_test.py rename web/regression/{utils => feature_utils}/pgadmin_page.py (95%) delete mode 100644 web/regression/utils/__init__.py diff --git a/web/pgadmin/acceptance/__init__.py b/web/pgadmin/feature_tests/__init__.py similarity index 100% rename from web/pgadmin/acceptance/__init__.py rename to web/pgadmin/feature_tests/__init__.py diff --git a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py b/web/pgadmin/feature_tests/connect_to_server_feature_test.py similarity index 78% rename from web/pgadmin/acceptance/tests/connect_to_server_feature_test.py rename to web/pgadmin/feature_tests/connect_to_server_feature_test.py index c3bee4e6..2a7f638e 100644 --- a/web/pgadmin/acceptance/tests/connect_to_server_feature_test.py +++ b/web/pgadmin/feature_tests/connect_to_server_feature_test.py @@ -7,29 +7,20 @@ # ############################################################## -from selenium import webdriver from selenium.webdriver import ActionChains import config as app_config -from pgadmin.utils.route import BaseTestGenerator from regression import test_utils -from regression.utils.app_starter import AppStarter -from regression.utils.pgadmin_page import PgadminPage +from regression.feature_utils.base_feature_test import BaseFeatureTest -class ConnectsToServerFeatureTest(BaseTestGenerator): +class ConnectsToServerFeatureTest(BaseFeatureTest): """ Tests that a database connection can be created from the UI """ def setUp(self): - if app_config.SERVER_MODE: - self.skipTest("Currently, config is set to start pgadmin in server mode. " - "This test doesn't know username and password so doesn't work in server mode") - - driver = webdriver.Chrome() - self.app_starter = AppStarter(driver, app_config) - self.page = PgadminPage(driver, app_config) + super(ConnectsToServerFeatureTest, self).setUp() connection = test_utils.get_db_connection(self.server['db'], self.server['username'], @@ -40,9 +31,6 @@ class ConnectsToServerFeatureTest(BaseTestGenerator): test_utils.create_database(self.server, "acceptance_test_db") test_utils.create_table(self.server, "acceptance_test_db", "test_table") - self.app_starter.start_app() - self.page.wait_for_app() - def runTest(self): self.assertEqual(app_config.APP_NAME, self.page.driver.title) self.page.wait_for_spinner_to_disappear() @@ -61,10 +49,6 @@ class ConnectsToServerFeatureTest(BaseTestGenerator): self.server['port']) test_utils.drop_database(connection, "acceptance_test_db") - def failureException(self, *args, **kwargs): - self.page.driver.save_screenshot('/tmp/pgadmin_connect_to_server_test_failure.png') - return AssertionError(*args, **kwargs) - def _connects_to_server(self): self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click() self.page.driver.find_element_by_link_text("Object").click() diff --git a/web/pgadmin/acceptance/tests/template_selection_feature_test.py b/web/pgadmin/feature_tests/template_selection_feature_test.py similarity index 74% rename from web/pgadmin/acceptance/tests/template_selection_feature_test.py rename to web/pgadmin/feature_tests/template_selection_feature_test.py index b7405d56..858950f4 100644 --- a/web/pgadmin/acceptance/tests/template_selection_feature_test.py +++ b/web/pgadmin/feature_tests/template_selection_feature_test.py @@ -1,22 +1,13 @@ from selenium import webdriver from selenium.webdriver import ActionChains -import config as app_config -from pgadmin.utils.route import BaseTestGenerator from regression import test_utils -from regression.utils.app_starter import AppStarter -from regression.utils.pgadmin_page import PgadminPage +from regression.feature_utils.base_feature_test import BaseFeatureTest -class TemplateSelectionFeatureTest(BaseTestGenerator): +class TemplateSelectionFeatureTest(BaseFeatureTest): def setUp(self): - if app_config.SERVER_MODE: - self.skipTest("Currently, config is set to start pgadmin in server mode. " - "This test doesn't know username and password so doesn't work in server mode") - - driver = webdriver.Chrome() - self.app_starter = AppStarter(driver, app_config) - self.page = PgadminPage(driver, app_config) + super(TemplateSelectionFeatureTest, self).setUp() connection = test_utils.get_db_connection(self.server['db'], self.server['username'], @@ -27,9 +18,6 @@ class TemplateSelectionFeatureTest(BaseTestGenerator): test_utils.create_database(self.server, "acceptance_test_db") - self.app_starter.start_app() - self.page.wait_for_app() - self.page.add_server(self.server) def runTest(self): @@ -71,8 +59,4 @@ $BODY$ self.server['db_password'], self.server['host'], self.server['port']) - test_utils.drop_database(connection, "acceptance_test_db") - - def failureException(self, *args, **kwargs): - self.page.driver.save_screenshot('/tmp/pgadmin_sql_template_selection_failure.png') - return AssertionError(*args, **kwargs) + test_utils.drop_database(connection, "acceptance_test_db") \ No newline at end of file diff --git a/web/pgadmin/utils/route.py b/web/pgadmin/utils/route.py index 996892a6..2dea25d9 100644 --- a/web/pgadmin/utils/route.py +++ b/web/pgadmin/utils/route.py @@ -48,7 +48,7 @@ class TestsGeneratorRegistry(ABCMeta): # Register this type of module, based on the module name # Avoid registering the BaseDriver itself - if name != 'BaseTestGenerator': + if name != 'BaseTestGenerator' and name != 'BaseFeatureTest': TestsGeneratorRegistry.registry[d['__module__']] = cls ABCMeta.__init__(cls, name, bases, d) diff --git a/web/regression/README b/web/regression/README index 7101eb75..d16111b6 100644 --- a/web/regression/README +++ b/web/regression/README @@ -103,7 +103,7 @@ Test Data Details Execution: ----------- -- For acceptance tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: +- For feature tests to run as part of the entire test suite, Chrome and chromedriver need to be installed: get chromedriver from https://sites.google.com/a/chromium.org/chromedriver/downloads or a package manager and make sure it is on the PATH @@ -137,8 +137,8 @@ Execution: - Exclude a package and its subpackages when running tests: - Example: exclude acceptance tests but run all others: - run 'python runtests.py --exclude acceptance' + Example: exclude feature tests but run all others: + run 'python runtests.py --exclude feature_tests' Example: exclude multiple packages: run 'python runtests.py --exclude browser.server_groups.servers.databases,browser.server_groups.servers.tablespaces' diff --git a/web/pgadmin/acceptance/tests/__init__.py b/web/regression/feature_utils/__init__.py similarity index 100% rename from web/pgadmin/acceptance/tests/__init__.py rename to web/regression/feature_utils/__init__.py diff --git a/web/regression/utils/app_starter.py b/web/regression/feature_utils/app_starter.py similarity index 100% rename from web/regression/utils/app_starter.py rename to web/regression/feature_utils/app_starter.py diff --git a/web/regression/feature_utils/base_feature_test.py b/web/regression/feature_utils/base_feature_test.py new file mode 100644 index 00000000..62d3bb36 --- /dev/null +++ b/web/regression/feature_utils/base_feature_test.py @@ -0,0 +1,26 @@ +from selenium import webdriver + +import config as app_config +from pgadmin.utils.route import BaseTestGenerator +from regression.feature_utils.app_starter import AppStarter +from regression.feature_utils.pgadmin_page import PgadminPage + + +class BaseFeatureTest(BaseTestGenerator): + def setUp(self): + if app_config.SERVER_MODE: + self.skipTest("Currently, config is set to start pgadmin in server mode. " + "This test doesn't know username and password so doesn't work in server mode") + + driver = webdriver.Chrome() + self.app_starter = AppStarter(driver, app_config) + self.page = PgadminPage(driver, app_config) + self.app_starter.start_app() + self.page.wait_for_app() + + def failureException(self, *args, **kwargs): + self.page.driver.save_screenshot('/tmp/feature_test_failure.png') + return AssertionError(*args, **kwargs) + + def runTest(self): + pass \ No newline at end of file diff --git a/web/regression/utils/pgadmin_page.py b/web/regression/feature_utils/pgadmin_page.py similarity index 95% rename from web/regression/utils/pgadmin_page.py rename to web/regression/feature_utils/pgadmin_page.py index d6a5836c..8d2843f6 100644 --- a/web/regression/utils/pgadmin_page.py +++ b/web/regression/feature_utils/pgadmin_page.py @@ -1,4 +1,5 @@ import time + from selenium.common.exceptions import NoSuchElementException from selenium.webdriver import ActionChains from selenium.webdriver.common.keys import Keys @@ -65,6 +66,9 @@ class PgadminPage: "//pre[contains(@class,'CodeMirror-line')]/../../../*[contains(@class,'CodeMirror-code')]").click() ActionChains(self.driver).send_keys(field_content).perform() + def click_tab(self, tab_name): + self.find_by_xpath("//*[contains(@class,'wcPanelTab') and contains(.,'" + tab_name + "')]").click() + def wait_for_input_field_content(self, field_name, content): def input_field_has_content(): element = self.driver.find_element_by_xpath( @@ -117,4 +121,4 @@ class PgadminPage: time_waited += sleep_time time.sleep(sleep_time) - raise RuntimeError("timed out waiting for " + waiting_for_message) + raise AssertionError("timed out waiting for " + waiting_for_message) diff --git a/web/regression/test_utils.py b/web/regression/test_utils.py index b4445d7d..1c2d244d 100644 --- a/web/regression/test_utils.py +++ b/web/regression/test_utils.py @@ -157,6 +157,11 @@ def drop_database(connection, database_name): """This function used to drop the database""" if database_name not in ["postgres", "template1", "template0"]: pg_cursor = connection.cursor() + + pg_cursor.execute( + "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity " + "WHERE pg_stat_activity.datname ='%s' and pid <> pg_backend_pid();" % database_name + ) pg_cursor.execute("SELECT * FROM pg_database db WHERE" " db.datname='%s'" % database_name) if pg_cursor.fetchall(): diff --git a/web/regression/utils/__init__.py b/web/regression/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 -- 2.11.0 [application/octet-stream] 0004-wrap-README-in-80-characters-and-simplify-TemplateSe.patch (3.2K, 4-0004-wrap-README-in-80-characters-and-simplify-TemplateSe.patch) download | inline diff: From 186e6fda2557e23346a14340364187bd0fc8dcee Mon Sep 17 00:00:00 2001 From: Sarah McAlear and Tira Odhner <[email protected]> Date: Tue, 21 Feb 2017 15:42:45 -0500 Subject: [PATCH 2/2] wrap README in 80 characters and simplify TemplateSelectionFeatureTest --- README | 13 +++++++------ .../feature_tests/template_selection_feature_test.py | 11 +---------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/README b/README index c4a533e2..ed010174 100644 --- a/README +++ b/README @@ -2,7 +2,7 @@ pgAdmin 4 ========= pgAdmin 4 is a rewrite of the popular pgAdmin3 management tool for the -PostgreSQL (http://www.postgresql.org) database. +PostgreSQL (http://www.postgresql.org) database. In the following documentation and examples, "$PGADMIN4_SRC/" is used to denote the top-level directory of a copy of the pgAdmin source tree, either from a @@ -53,9 +53,10 @@ By default, the runtime application will be built in release mode. On Linux, an executable called 'pgAdmin4' will be built, and on Mac OS X, an app bundle called pgAdmin4.app will be created. -To build the runtime on a Windows system, export PYTHON_HOME and PYTHON_VERSION -variables in the System environment. Specify the PYTHON_VERSION with the major -and minor number. Do not specify micro level version. +To build the runtime on a Windows system, export PYTHON_HOME and +PYTHON_VERSION variables in the System environment. Specify the +PYTHON_VERSION with the major and minor number. Do not specify micro level +version. For example, given a Python version of A.B.C; A - Major number, B - Minor number, C - Micro level (Bug fix releases). @@ -100,7 +101,7 @@ process is fairly simple - adapt as required for your distribution: pg_config can be found for building psycopg2), and install the required packages: - (pgadmin4) $ PATH=$PATH:/usr/local/pgsql/bin pip install -r $PGADMIN4_SRC/requirements_py2.txt + $ PATH=$PATH:/usr/local/pgsql/bin pip install -r $PGADMIN4_SRC/requirements_py2.txt If you are using Python 3, use the requirements_py3.txt file instead. @@ -302,6 +303,6 @@ pgAdmin Hackers mailing list: [email protected] --- +-- Dave Page pgAdmin Project Lead diff --git a/web/pgadmin/feature_tests/template_selection_feature_test.py b/web/pgadmin/feature_tests/template_selection_feature_test.py index 858950f4..ceb12478 100644 --- a/web/pgadmin/feature_tests/template_selection_feature_test.py +++ b/web/pgadmin/feature_tests/template_selection_feature_test.py @@ -36,16 +36,7 @@ class TemplateSelectionFeatureTest(BaseFeatureTest): self.page.find_by_partial_link_text("Trigger function...").click() self.page.fill_input_by_field_name("name", "test-trigger-function") self.page.find_by_partial_link_text("Definition").click() - self.page.fill_codemirror_area_with( -"""CREATE OR REPLACE FUNCTION log_last_name_changes() -RETURNS TRIGGER AS -$BODY$ -BEGIN - -END; -$BODY$ -""" - ) + self.page.fill_codemirror_area_with("some-trigger-function-content") self.page.find_by_partial_link_text("SQL").click() self.page.find_by_xpath("//*[contains(@class,'CodeMirror-lines') and contains(.,'LEAKPROOF')]") -- 2.11.0 ^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: Acceptance Tests against a browser (WIP) @ 2017-02-22 12:42 Dave Page <[email protected]> parent: Atira Odhner <[email protected]> 0 siblings, 0 replies; 30+ messages in thread From: Dave Page @ 2017-02-22 12:42 UTC (permalink / raw) To: Atira Odhner <[email protected]>; +Cc: pgadmin-hackers; Sarah McAlear <[email protected]> Thanks, patch applied! On Tue, Feb 21, 2017 at 10:12 PM, Atira Odhner <[email protected]> wrote: > Hi Dave, > > We fixed the flakiness issues that we saw (hopefully they are the same ones > you were seeing.) by tearing down connections to the acceptance_test_db > before attempting to drop it at the beginning of the test. Once we have > access to the CI pipeline we can help out there to ensure the flakiness is > gone. > > We wrapped the README at 80 characters, and removed the misleading function > definition from the test. > > As far as the screenshots go, I'm more inclined to remove the screenshotting > than to work on improving it. It currently only works when the failure is > due to an AssertionError since that's what failureException relies on. > > We also renamed acceptance to feature_tests since 'acceptance' seemed > ambiguous/redundant with 'regression'. > > Tira & Sara > > > On Mon, Feb 13, 2017 at 9:36 AM, Dave Page <[email protected]> wrote: >> >> Hi, >> >> I've been playing with this for the last couple of hours, and I just >> can't get it to work reliably; >> >> - A good percentage of the time the browser opens with a URL of >> "data:," and does nothing more. This appears to happen if tests fail, >> which still leaves server processes running in the background. >> >> - The connect_to_server test usually seems to work. >> >> - The template_selection_feature test usually does *not* work. I can't >> see an obvious reason, but I suspect it's a race condition. What seems >> to happen is that the function definition is entered, but not >> registered by the UI, so the mSQL panel just ends up saying >> "incomplete definition". Manually checking what was input proves that >> everything is correct - and indeed, returning the SQL tab shows the >> expected SQL. >> >> Other issues I noted: >> >> - The template_selection_feature test should just enter BEGIN/END. >> What it currently enters is an entire function definition, when only >> the body content is expected. E.g. >> >> self.page.fill_codemirror_area_with( >> """BEGIN >> >> END; >> """ >> ) >> >> - Screenshots are being taken of failed tests: >> 1) I've never actually seen any get saved >> 2) They should be saved to the same directory as the test log, not /tmp >> 3) They should have guaranteed unique names, and be mentioned in the >> test output so the user can reference the image to the failure. >> >> The reason the last two items are important is that I've now got a >> test server running the test suite with every supported version of >> Python, for every supported database (well, almost, pending a couple >> of fixes). I have separate workspaces for each Python version, and a >> single test run might run every test 10 times, once for each database >> server. >> >> - Please wrap the README at < 80 chars. >> >> >> >> On Thu, Feb 9, 2017 at 4:17 PM, Atira Odhner <[email protected]> wrote: >> > Hi Dave, >> > >> >> I think the problem was that the way you phrased it, >> > >> > >> > You're right, we totally messed that up. We were talking about making 3 >> > patches and ended up making only 2 and forgot to reword that bit. >> > Sorry about that. >> > >> > Here are the two patches for this change that resolves the >> > AttributeError >> > you were seeing. The first patch is identical to the patch of the same >> > name >> > in the other email thread. >> > >> >> We're used to >> >> dealing with larger patchsets via the mailing list - typically as long >> >> as you're clear about any dependencies, it shouldn't be a problem. >> > >> > >> > Great! We'll try sending patchsets from now on and hopefully that >> > resolves >> > some of the issues we were seeing. >> > >> > Tira & George >> > >> > On Thu, Feb 9, 2017 at 9:28 AM, Dave Page <[email protected]> wrote: >> >> >> >> Hi >> >> >> >> On Thu, Feb 9, 2017 at 2:20 PM, Atira Odhner <[email protected]> >> >> wrote: >> >> > Certainly. We did mention the dependency in the email. Would it be >> >> > better >> >> > to mention it in the patch name? >> >> >> >> I think the problem was that the way you phrased it, it sounded >> >> optional ("an updated patch which does not include adding that test >> >> helper in case you apply the show-tables patch first"). I think a >> >> clear "This patch is dependent on patch Foo" would suffice. >> >> >> >> > Is there a better way for us to manage >> >> > these changes? On other open source projects, I've seen github >> >> > mirrors >> >> > set >> >> > up so that changes can be pulled in like branches rather then as >> >> > patch >> >> > applies. That would have avoided this situation since the parent >> >> > commit >> >> > would be pulled in with the same SHA from either pull request branch >> >> > and >> >> > git >> >> > would not see it as a conflict. >> >> > >> >> > I'm rather new to dealing with patch files like this so I would love >> >> > some >> >> > tips. >> >> >> >> The Postgres project in general is quite conservative and stuck in >> >> it's ways about how things are done (which is usually a good thing >> >> considering you trust your data to the resulting code). We're used to >> >> dealing with larger patchsets via the mailing list - typically as long >> >> as you're clear about any dependencies, it shouldn't be a problem. >> >> Some of us use tools like PyCharms for handling patches and helping >> >> with reviews etc. which I guess replaces most, if not all of the >> >> GitHub functionality over plain git. >> >> >> >> -- >> >> Dave Page >> >> Blog: http://pgsnake.blogspot.com >> >> Twitter: @pgsnake >> >> >> >> EnterpriseDB UK: http://www.enterprisedb.com >> >> The Enterprise PostgreSQL Company >> > >> > >> >> >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company > > -- Dave Page Blog: http://pgsnake.blogspot.com Twitter: @pgsnake EnterpriseDB UK: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers ^ permalink raw reply [nested|flat] 30+ messages in thread
end of thread, other threads:[~2017-02-22 12:42 UTC | newest] Thread overview: 30+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2017-01-17 16:09 Re: Acceptance Tests against a browser (WIP) Atira Odhner <[email protected]> 2017-01-19 22:07 ` George Gelashvili <[email protected]> 2017-01-19 23:15 ` George Gelashvili <[email protected]> 2017-01-20 15:38 ` Dave Page <[email protected]> 2017-01-20 15:38 ` Dave Page <[email protected]> 2017-01-20 17:33 ` George Gelashvili <[email protected]> 2017-01-24 09:43 ` Dave Page <[email protected]> 2017-01-25 23:31 ` George Gelashvili <[email protected]> 2017-01-26 22:40 ` George Gelashvili <[email protected]> 2017-01-27 16:11 ` Dave Page <[email protected]> 2017-01-27 16:28 ` Dave Page <[email protected]> 2017-01-30 19:28 ` George Gelashvili <[email protected]> 2017-01-30 21:23 ` Atira Odhner <[email protected]> 2017-01-31 14:41 ` Dave Page <[email protected]> 2017-01-31 14:54 ` George Gelashvili <[email protected]> 2017-01-31 15:10 ` Dave Page <[email protected]> 2017-01-31 16:25 ` Dave Page <[email protected]> 2017-02-03 21:56 ` Atira Odhner <[email protected]> 2017-02-06 10:32 ` Dave Page <[email protected]> 2017-02-06 14:54 ` Atira Odhner <[email protected]> 2017-02-08 22:15 ` Atira Odhner <[email protected]> 2017-02-09 12:47 ` Dave Page <[email protected]> 2017-02-09 13:15 ` Atira Odhner <[email protected]> 2017-02-09 13:26 ` Dave Page <[email protected]> 2017-02-09 14:20 ` Atira Odhner <[email protected]> 2017-02-09 14:28 ` Dave Page <[email protected]> 2017-02-09 16:17 ` Atira Odhner <[email protected]> 2017-02-13 14:36 ` Dave Page <[email protected]> 2017-02-21 22:12 ` Atira Odhner <[email protected]> 2017-02-22 12:42 ` Dave Page <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox