From 874087ef3f3d5f6df5d965ef7fb3f5e897516fad Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Fri, 4 Aug 2023 21:18:12 +0200 Subject: [PATCH 1/7] Adding end-to-end test for the QLever UI So far, the QLever UI code has zero continuous integration tests. The first step is to add a test that checks that the page is shown propery and some basic functionality. --- end2end-test.py | 136 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 end2end-test.py diff --git a/end2end-test.py b/end2end-test.py new file mode 100644 index 00000000..1e85dcca --- /dev/null +++ b/end2end-test.py @@ -0,0 +1,136 @@ +#!/usr/bin/python3 +""" +Copyright 2023, University of Freiburg, +Chair of Algorithms and Data Structures +Author: Hannah Bast +""" + +import selenium +from selenium import webdriver +from selenium.webdriver.firefox.options import Options +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support.ui import Select +from selenium.webdriver.support import expected_conditions as EC + +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.by import By +from selenium.webdriver.common.action_chains import ActionChains + +import argparse +import logging +import sys +import re + + +# Global log with custom formatter, inspired by several posts on Stackoverflow. +class MyFormatter(logging.Formatter): + def __init__(self): + super().__init__(datefmt="%Y-%m-%d %H:%M:%S") + def format(self, record): + format_orig = self._style._fmt + fmt_begin, fmt_end = "", "" + if record.levelno == logging.ERROR: + fmt_begin, fmt_end = "\x1b[31m", "\x1b[0m" + elif record.levelno == logging.WARN: + fmt_begin, fmt_end = "\x1b[35m", "\x1b[0m" + fmt = "%(asctime)s.%(msecs)03d %(levelname)-5s %(message)s" + self._style._fmt = fmt_begin + fmt + fmt_end + result = logging.Formatter.format(self, record) + self._style._fmt = format_orig + return result + +log = logging.getLogger("e2e test logger") +log.setLevel(logging.INFO) +handler = logging.StreamHandler() +handler.setFormatter(MyFormatter()) +# handler.setFormatter(logging.Formatter( +# "%(asctime)s.%(msecs)03d %(levelname)-5s %(message)s", "%Y-%m-%d %H:%M:%S")) +log.addHandler(handler) + +class QleverUiTester: + """ + Class or testing the Qlever UI. + + NOTE: The basic structure of this code is taken from + https://github.com/ad-freiburg/hisinone-scraper + """ + + def __init__(self, headless, url, num_retries): + """ + Basic settings and open the browser window (using Firefox). + """ + + self.url = url + self.timeout_loading = 5 + self.num_retries = num_retries + self.headless = headless + options = Options() + if headless == True: + options.headless = True + self.driver = webdriver.Firefox(options=options) + self.driver.set_window_position(100, 0) + self.driver.set_window_size(1400, 600) + + def done(self): + """ + Close the browser window if it's still there. + """ + + try: + self.driver.close() + except: + pass + + def test(self): + """ + Some basic tests to check if the UI is working. + """ + + for i in range(self.num_retries): + try: + self.driver.get(self.url) + WebDriverWait(self.driver, self.timeout_loading).until( + EC.presence_of_element_located((By.ID, "query"))) + except: + if i < self.num_retries - 1: + log.info("Loading page failed, retrying...") + else: + log.error("Aborting after %d retries." % self.num_retries) + self.done() + sys.exit(1) + + +if __name__ == "__main__": + + # Setup parser and basic usage information. + parser = MyArgumentParser( + epilog="Example invocation: python3 hisinone-scraper", + formatter_class=argparse.RawDescriptionHelpFormatter) + + # Command line arguments. + parser.add_argument( + "--headless", dest="headless", action="store_true", + default="False", help="Run browser in headless mode (default: no)") + parser.add_argument( + "--url", dest="url", type=str, + default="https://qlever.cs.uni-freiburg.de", + help="The URL of the QLever UI (may redirect)") + parser.add_argument( + "--num-retries", dest="num_retries", type=int, default=5, + help="Number of retries for loading a page") + parser.add_argument( + "--log-level", dest="log_level", type=str, + choices=["INFO", "DEBUG", "ERROR"], default="INFO", + help="Log level (INFO, DEBUG, ERROR)") + args = parser.parse_args(sys.argv[1:]) + + # Set log level and show it. + log.setLevel(eval("logging.%s" % args.log_level)) + print() + log.info("Log level is \x1b[1m%s\x1b[0m" % args.log_level) + log.info("Headless is \x1b[1m%s\x1b[0m" % args.headless) + + # Test the QLever UI. + qleverui_tester = QleverUiTester(args.headless, args.url, args.num_retries) + qleverui_tester.test() + qleverui_tester.done() From 41cb9002d425bb3b37dca9b780c78c520378d055 Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Fri, 4 Aug 2023 22:22:08 +0200 Subject: [PATCH 2/7] First working version of test (with webdriver.Firefox) --- db/qleverui.sqlite3 | Bin 292864 -> 294912 bytes end2end-test.py | 54 +++++++++++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/db/qleverui.sqlite3 b/db/qleverui.sqlite3 index a96562b418c815ab756fedbdbab9ac430f2131ab..24cf8aee811682929d52dba0ec4a9caf55d233f4 100644 GIT binary patch delta 2012 zcmai#OK;;;6vq?Woi<1yW;6mMG|od5rv%fD}4 zUf%lT&8-hIne6VTyYHtz@}26p>R0c+vXx2C@W+eiF8=u8&D($Bcd~D9zmy*2u&WII zJ^m|xfZxL%{4?wq>1KCukkw*sugY$+FHBtjRRHwN` zlIT!NT)vjW{>?-gPmj`a|Afun!V6hQLTZoX_5imF zNdW=@@p+2jD4xl^7zRjyC{HsKq?ny+!n6QDo<$JyX#3hQN#+5^rmw$|hG)-x98dzy z=K;y5_qSfWP9lK>c^W~EVTo(Pv_PXgM9_%&flfJ~kSDIJO3!3#M^zOnkx$@Dsv zXDJo{_|esatL%d-?3*k6$1iRZ#I5a2J%cCMBh1eJp8YWMbEf|J-5q{5C$_ha=C2nv z2~A6?=6UBhxac6eVtI655`fwfzw$84dP6y(3A&WWuh$0oO|<0uPoTuWH}OsTt_JxA7faeB^JjcKP& zBB{!E9BE%AS?DBj(}v`1wSTMPnG?j4W0N7*a6n7g8v+8SeiAh)730 delta 53 zcmZo@5Nh}!I6;~} IvMtO<0K-TU!2kdN diff --git a/end2end-test.py b/end2end-test.py index 1e85dcca..e6ba69a5 100644 --- a/end2end-test.py +++ b/end2end-test.py @@ -5,27 +5,24 @@ Author: Hannah Bast """ -import selenium from selenium import webdriver from selenium.webdriver.firefox.options import Options +# from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support.ui import Select from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By -from selenium.webdriver.common.action_chains import ActionChains import argparse import logging import sys -import re # Global log with custom formatter, inspired by several posts on Stackoverflow. class MyFormatter(logging.Formatter): def __init__(self): super().__init__(datefmt="%Y-%m-%d %H:%M:%S") + def format(self, record): format_orig = self._style._fmt fmt_begin, fmt_end = "", "" @@ -39,14 +36,14 @@ def format(self, record): self._style._fmt = format_orig return result + log = logging.getLogger("e2e test logger") log.setLevel(logging.INFO) handler = logging.StreamHandler() handler.setFormatter(MyFormatter()) -# handler.setFormatter(logging.Formatter( -# "%(asctime)s.%(msecs)03d %(levelname)-5s %(message)s", "%Y-%m-%d %H:%M:%S")) log.addHandler(handler) + class QleverUiTester: """ Class or testing the Qlever UI. @@ -57,17 +54,22 @@ class QleverUiTester: def __init__(self, headless, url, num_retries): """ - Basic settings and open the browser window (using Firefox). + Basic settings and open the browser window. """ + self.headless = headless self.url = url - self.timeout_loading = 5 self.num_retries = num_retries - self.headless = headless + self.timeout_loading = 5 options = Options() - if headless == True: + if self.headless: + log.info("Running in \x1b[1mheadless\x1b[0m mode") options.headless = True + else: + log.info("Not headless, rerun with --headless to activate") + log.info("Initializing webdriver ...") self.driver = webdriver.Firefox(options=options) + # self.driver = webdriver.Chrome(options=options) self.driver.set_window_position(100, 0) self.driver.set_window_size(1400, 600) @@ -78,7 +80,7 @@ def done(self): try: self.driver.close() - except: + except Exception: pass def test(self): @@ -91,15 +93,31 @@ def test(self): self.driver.get(self.url) WebDriverWait(self.driver, self.timeout_loading).until( EC.presence_of_element_located((By.ID, "query"))) - except: + log.info(f"Page {self.url} loaded successfully") + break + except Exception: if i < self.num_retries - 1: - log.info("Loading page failed, retrying...") + log.info(f"Loading page failed" + f" (attempt {i + 1} of {self.num_retries})" + f", trying again ...") else: log.error("Aborting after %d retries." % self.num_retries) self.done() sys.exit(1) +class MyArgumentParser(argparse.ArgumentParser): + """ + Override the error message so that it prints the full help text if the + script is called without arguments or with a wrong argument. + """ + + def error(self, message): + print("ArgumentParser: %s\n" % message) + self.print_help() + sys.exit(1) + + if __name__ == "__main__": # Setup parser and basic usage information. @@ -109,8 +127,8 @@ def test(self): # Command line arguments. parser.add_argument( - "--headless", dest="headless", action="store_true", - default="False", help="Run browser in headless mode (default: no)") + "--not-headless", dest="not_headless", action="store_true", + help="Run browser in headful mode (default: headless mode)") parser.add_argument( "--url", dest="url", type=str, default="https://qlever.cs.uni-freiburg.de", @@ -128,9 +146,9 @@ def test(self): log.setLevel(eval("logging.%s" % args.log_level)) print() log.info("Log level is \x1b[1m%s\x1b[0m" % args.log_level) - log.info("Headless is \x1b[1m%s\x1b[0m" % args.headless) # Test the QLever UI. - qleverui_tester = QleverUiTester(args.headless, args.url, args.num_retries) + qleverui_tester = QleverUiTester( + not args.not_headless, args.url, args.num_retries) qleverui_tester.test() qleverui_tester.done() From ab938638fc085f9ad11e4cfe7739a838bfcca7a9 Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Fri, 4 Aug 2023 22:52:12 +0200 Subject: [PATCH 3/7] Add workflow (will not work yet because install stuff missing) --- .github/workflows/ui-check.yml | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/ui-check.yml diff --git a/.github/workflows/ui-check.yml b/.github/workflows/ui-check.yml new file mode 100644 index 00000000..e4164da1 --- /dev/null +++ b/.github/workflows/ui-check.yml @@ -0,0 +1,35 @@ +name: UI check + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + merge_group: + +jobs: + build: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + # See https://docs.docker.com/engine/install/ubuntu/ + run: | + sudo apt install -y apt-transport-https ca-certificates curl software-properties-common + sudo apt remove docker docker-engine docker.io containerd runc + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable" + sudo apt update + sudo apt-cache policy docker-ce + sudo apt install docker-ce containerd.io + + - name: Docker build and run + run: | + sudo docker build -t qlever-ui ${{github.workspace}} + docker run --it -d -v $(pwd)/db:/app/db --name qlever-ui qlever-ui + + - name: End-to-end test + run: | + python3 end2end-test.py --url http://localhost:7000 + docker rm -f qlever-ui From 116b3190ed956a311de71fa83a666b4a6bbe5f65 Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Fri, 4 Aug 2023 22:55:06 +0200 Subject: [PATCH 4/7] Minor fix (--it -> -it) --- .github/workflows/ui-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ui-check.yml b/.github/workflows/ui-check.yml index e4164da1..b5dd3884 100644 --- a/.github/workflows/ui-check.yml +++ b/.github/workflows/ui-check.yml @@ -27,7 +27,7 @@ jobs: - name: Docker build and run run: | sudo docker build -t qlever-ui ${{github.workspace}} - docker run --it -d -v $(pwd)/db:/app/db --name qlever-ui qlever-ui + docker run -it -d -v $(pwd)/db:/app/db --name qlever-ui qlever-ui - name: End-to-end test run: | From 7138636a6677086b7c008f2f011846ab05ba7551 Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Fri, 4 Aug 2023 23:12:19 +0200 Subject: [PATCH 5/7] Some modification (again) --- .github/workflows/ui-check.yml | 22 +++++++++++++--------- end2end-test.py | 8 +++++--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ui-check.yml b/.github/workflows/ui-check.yml index b5dd3884..11d9a5d0 100644 --- a/.github/workflows/ui-check.yml +++ b/.github/workflows/ui-check.yml @@ -14,8 +14,8 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install dependencies - # See https://docs.docker.com/engine/install/ubuntu/ run: | + # See https://docs.docker.com/engine/install/ubuntu/ sudo apt install -y apt-transport-https ca-certificates curl software-properties-common sudo apt remove docker docker-engine docker.io containerd runc curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - @@ -23,13 +23,17 @@ jobs: sudo apt update sudo apt-cache policy docker-ce sudo apt install docker-ce containerd.io + # Install packages for use of Selenium + pip3 install selenium - - name: Docker build and run - run: | - sudo docker build -t qlever-ui ${{github.workspace}} - docker run -it -d -v $(pwd)/db:/app/db --name qlever-ui qlever-ui + - name: Build docker image for QLever UI + run: sudo docker build -t qlever-ui ${{github.workspace}} - - name: End-to-end test - run: | - python3 end2end-test.py --url http://localhost:7000 - docker rm -f qlever-ui + - name: Run docker image for QLever UI + run: docker run -it -d -v $(pwd)/db:/app/db --name qlever-ui qlever-ui + + - name: Test the QLever UI using Selenium + run: python3 end2end-test.py --url http://localhost:7000 + + - name: Remove the docker image + run: docker rm -f qlever-ui diff --git a/end2end-test.py b/end2end-test.py index e6ba69a5..0f752417 100644 --- a/end2end-test.py +++ b/end2end-test.py @@ -7,6 +7,7 @@ from selenium import webdriver from selenium.webdriver.firefox.options import Options +from selenium.webdriver.firefox.firefox_binary import FirefoxBinary # from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC @@ -64,14 +65,15 @@ def __init__(self, headless, url, num_retries): options = Options() if self.headless: log.info("Running in \x1b[1mheadless\x1b[0m mode") - options.headless = True + options.add_argument("-headless") else: log.info("Not headless, rerun with --headless to activate") log.info("Initializing webdriver ...") + # options.binary = FirefoxBinary("/usr/bin/firefox") self.driver = webdriver.Firefox(options=options) # self.driver = webdriver.Chrome(options=options) - self.driver.set_window_position(100, 0) - self.driver.set_window_size(1400, 600) + # self.driver.set_window_position(100, 0) + # self.driver.set_window_size(1400, 600) def done(self): """ From c8e21fd5f9214d67b9c75412c910e43e76c72423 Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Fri, 4 Aug 2023 23:27:20 +0200 Subject: [PATCH 6/7] Show exception when driver.get fails --- end2end-test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/end2end-test.py b/end2end-test.py index 0f752417..74f28251 100644 --- a/end2end-test.py +++ b/end2end-test.py @@ -97,10 +97,11 @@ def test(self): EC.presence_of_element_located((By.ID, "query"))) log.info(f"Page {self.url} loaded successfully") break - except Exception: + except Exception as e: if i < self.num_retries - 1: log.info(f"Loading page failed" - f" (attempt {i + 1} of {self.num_retries})" + f" (attempt {i + 1} of {self.num_retries}" + f", error: \"{str(e)}\")" f", trying again ...") else: log.error("Aborting after %d retries." % self.num_retries) From 2a8b5e168f09ccdf123bff9e4536ef6690048713 Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Fri, 4 Aug 2023 23:32:37 +0200 Subject: [PATCH 7/7] Expose port --- .github/workflows/ui-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ui-check.yml b/.github/workflows/ui-check.yml index 11d9a5d0..b8509f31 100644 --- a/.github/workflows/ui-check.yml +++ b/.github/workflows/ui-check.yml @@ -30,7 +30,7 @@ jobs: run: sudo docker build -t qlever-ui ${{github.workspace}} - name: Run docker image for QLever UI - run: docker run -it -d -v $(pwd)/db:/app/db --name qlever-ui qlever-ui + run: docker run -it -d -p 7000:7000 -v $(pwd)/db:/app/db --name qlever-ui qlever-ui - name: Test the QLever UI using Selenium run: python3 end2end-test.py --url http://localhost:7000