diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..472ba60 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,27 @@ +# Python CircleCI 2.0 configuration file +# Check https://circleci.com/docs/2.0/language-python/ for more details +# +version: 2 +jobs: + build: + docker: + - image: selenium/standalone-chrome:3.141 + + working_directory: ~/repo + + steps: + - checkout + + - run: + name: install dependencies + command: | + sudo apt-get update && \ + sudo apt-get install -y python3 python3-dev default-jdk python3-pillow python3-numpy python3-pip && \ + sudo apt-get install -y libjpeg-dev libfreetype6 libfreetype6-dev zlib1g-dev + sudo pip3 install selenium Pillow + + - run: + name: run tests + command: | + python3 utils/build/build.py + python3 tests/run_tests.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 328155d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: generic -dist: xenial -addons: - sauce_connect: true -before_install: - - sudo apt-get install -y python3-pillow python3-numpy python3-pip - - sudo pip3 install selenium -jobs: - include: - - stage: build - script: python3 utils/build/build.py - - stage: test - script: python3 tests/run_tests.py diff --git a/tests/drivers/chromedriver b/tests/drivers/chromedriver new file mode 100755 index 0000000..83e3f6d Binary files /dev/null and b/tests/drivers/chromedriver differ diff --git a/tests/drivers/geckodriver b/tests/drivers/geckodriver new file mode 100755 index 0000000..ba1da8c Binary files /dev/null and b/tests/drivers/geckodriver differ diff --git a/tests/run_tests.py b/tests/run_tests.py old mode 100644 new mode 100755 index efbdf1e..ea7e36e --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1,141 +1,239 @@ -""" -Selenium-based test suite for Pannellum +#!/usr/bin/env python -Dependencies: -Python 3, Selenium Python bindings, Firefox, geckodriver, Pillow, NumPy -""" +''' -import http.server -import time -import threading +Run tests for Pannellum, set up with Continuous Integration. +Contributed by Vanessa Sochat, JoSS Review 2019. +See the project repository for licensing information. + +''' + +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.keys import Keys +from random import choice +from threading import Thread +from selenium import webdriver +from http.server import SimpleHTTPRequestHandler +from socketserver import TCPServer +from PIL import Image, ImageChops +import argparse +import json import io -import subprocess import os -import numpy as np -from PIL import Image, ImageChops -from selenium import webdriver +import re +import shutil +import subprocess +import sys +import time +import webbrowser + + +class PannellumServer(SimpleHTTPRequestHandler): + '''here we subclass SimpleHTTPServer to capture error messages + ''' + def log_message(self, format, *args): + '''log to standard error with a date time string, + and then call any subclass specific logging functions + ''' + sys.stderr.write("%s - - [%s] %s\n" % + (self.address_string(), + self.log_date_time_string(), + format%args)) + + # Workaround for error trying to GET html + if not re.search("div", format%args) and not re.search("function", format%args): + if re.search("404", format%args): + raise IOError(format%args) + + def log_error(self, format, *args): + '''catch errors in the log_messages instead + ''' + pass + + +class PannellumTester(object): + ''' bring up a server with a testing robot + ''' + + def __init__(self, **kwargs): + self.Handler = PannellumServer + if "port" in kwargs: + self.port = kwargs['port'] + else: + self.port = choice(range(8000, 9999)) + print('Selected port is %s' % self.port) + self.httpd = TCPServer(("", self.port), self.Handler) + self.server = Thread(target=self.httpd.serve_forever) + self.server.setDaemon(True) + self.server.start() + self.started = True + self.pause_time = 100 + self.browser = None + self.headless = False + self.display = None + self.driver = "Chrome" + if "browser" in kwargs: + self.driver = kwargs['browser'] + + + def run_tests(self, create_ref=False): + '''run tests for Pannellum''' + + print("Loading page...") + self.get_page("http://localhost:%s/tests/tests.html" % self.port) + + print("Running tests...") + time.sleep(5) + + viewer = self.browser.find_element_by_id("panorama") + assert self.browser.execute_script("return viewer.isLoaded()") == True + + # Check equirectangular + assert self.browser.execute_script("return viewer.getScene() == 'equirectangular'") + + # Check movement + self.browser.execute_script("viewer.setPitch(30).setYaw(-20).setHfov(90)") + time.sleep(2) + assert self.browser.execute_script( + "return viewer.getPitch() == 30 && viewer.getYaw() == -20 && viewer.getHfov() == 90" + ) + self.browser.find_element_by_class_name("pnlm-zoom-in").click() + time.sleep(1) + assert self.browser.execute_script("return viewer.getHfov() == 85") + self.browser.find_element_by_class_name("pnlm-zoom-out").click() + time.sleep(1) + assert self.browser.execute_script("return viewer.getHfov() == 90") + print("PASS: movement") + + # Check look at + self.browser.execute_script("viewer.lookAt(-10, 90, 100)") + time.sleep(2) + assert self.browser.execute_script( + "return viewer.getPitch() == -10 && viewer.getYaw() == 90 && viewer.getHfov() == 100" + ) + print("PASS: look at") + + # Check cube + self.browser.execute_script("viewer.loadScene('cube')") + time.sleep(5) + assert self.browser.execute_script("return viewer.getScene() == 'cube'") + + # Check hot spot + self.browser.find_element_by_class_name("pnlm-scene").click() + time.sleep(5) + assert self.browser.execute_script("return viewer.getScene() == 'multires'") + print("PASS: hot spot") + + # Check multires + self.httpd.server_close() + + + def get_browser(self,name=None): + '''get_browser + return a browser if it hasn't been initialized yet + ''' + if name is None: + name=self.driver + + log_path = "%s-driver.log" % name.lower() + + if self.browser is None: + options = self.get_options() + if name.lower() == "Firefox": + self.browser = webdriver.Firefox(service_log_path=log_path) + else: + self.browser = webdriver.Chrome(service_log_path=log_path, + options=options) + return self.browser + + + def get_options(self, width=1200, height=800): + '''return options for headless, no-sandbox, and custom width/height + ''' + options = webdriver.ChromeOptions() + options.add_argument("headless") + options.add_argument("no-sandbox") + options.add_argument("window-size=%sx%s" %(width, height)) + return options + + + def get_page(self, url, name='Chrome'): + '''get_page + open a particular url, checking for Timeout + ''' + if self.browser is None: + self.browser = self.get_browser(name) + + try: + return self.browser.get(url) + except TimeoutException: + print('Browser request timeout. Are you connected to the internet?') + self.browser.close() + sys.exit(1) + + def stop(self): + '''close any running browser or server, and shut down the robot + ''' + if self.browser is not None: + self.browser.close() + self.httpd.server_close() + + if self.display is not None: + self.display.close() + + +## MAIN ######################################################################## + +def get_parser(): + + parser = argparse.ArgumentParser( + description="run tests for Pannellum") + + parser.add_argument("--port",'-p', dest='port', + help="port to run webserver", + type=int, default=3030) + + parser.add_argument("--headless", dest='headless', + help="start a display before browser", + action="store_true", default=False) + + parser.add_argument("--create-ref", dest='create_ref', + action="store_true", default=False) + + parser.add_argument("--browser",'-b', dest='browser', + choices=['Firefox', 'Chrome'], + help="browser driver to use for the robot", + type=str, default="Chrome") + return parser + +def main(): + + parser = get_parser() + + try: + args = parser.parse_args() + except: + sys.exit(0) + + # The drivers must be on path + here = os.path.abspath(os.path.dirname(__file__)) + os.environ['PATH'] = "%s/drivers:%s" %(here, os.environ['PATH']) + os.chdir(here) + + # We must be in root directory + os.chdir('../') + + # Iniitalize the tester + tester = PannellumTester(browser=args.browser, + port=args.port, + headless=args.headless) + + # Run tests + tester.run_tests(create_ref=args.create_ref) + # Clean up shop! + tester.stop() -# Set to true to create a new set of reference images -CREATE_REF = False - - -# Run web server -print("Starting web server...") -os.chdir(os.path.dirname(os.path.abspath(__file__))) # cd to script dir -os.chdir("..") -httpd = http.server.HTTPServer( - ("localhost", 8000), http.server.SimpleHTTPRequestHandler -) -thread = threading.Thread(None, httpd.serve_forever) -thread.start() - - -# Create a new instance of the Firefox driver -print("Starting web driver...") -if os.environ.get("TRAVIS_JOB_NUMBER"): - # Configuration for Travis CI / Sauce Labs testing - driver = webdriver.Remote( - command_executor="https://ondemand.saucelabs.com:443/wd/hub", - desired_capabilities={ - "username": os.environ["SAUCE_USERNAME"], - "accessKey": os.environ["SAUCE_ACCESS_KEY"], - "tunnel-identifier": os.environ["TRAVIS_JOB_NUMBER"], - "build": os.environ["TRAVIS_JOB_NUMBER"], - "browserName": "firefox", - "seleniumVersion": "3.141.0", - }, - ) -else: - fp = webdriver.FirefoxProfile() - fp.set_preference("layout.css.devPixelsPerPx", "1.0") - driver = webdriver.Firefox(firefox_profile=fp) - driver.set_window_size(800, 600) - - -def run_tests(): - # Load page - print("Loading page...") - driver.get("http://localhost:8000/tests/tests.html") - - # Make sure viewer loaded - print("Running tests...") - time.sleep(5) - viewer = driver.find_element_by_id("panorama") - assert driver.execute_script("return viewer.isLoaded()") == True - - # Check equirectangular - assert driver.execute_script("return viewer.getScene() == 'equirectangular'") - if CREATE_REF: - viewer.screenshot("tests/equirectangular.png") - subprocess.call(["optipng", "-o7", "-strip", "all", "equirectangular.png"]) - else: - reference = Image.open("tests/equirectangular.png") - screenshot = Image.open(io.BytesIO(viewer.screenshot_as_png)).convert("RGB") - diff = np.mean(np.array(ImageChops.difference(screenshot, reference))) - print("equirectangular difference:", diff) - assert diff < 3 - print("PASS: equirectangular") - - # Check movement - driver.execute_script("viewer.setPitch(30).setYaw(-20).setHfov(90)") - time.sleep(2) - assert driver.execute_script( - "return viewer.getPitch() == 30 && viewer.getYaw() == -20 && viewer.getHfov() == 90" - ) - driver.find_element_by_class_name("pnlm-zoom-in").click() - time.sleep(1) - assert driver.execute_script("return viewer.getHfov() == 85") - driver.find_element_by_class_name("pnlm-zoom-out").click() - time.sleep(1) - assert driver.execute_script("return viewer.getHfov() == 90") - print("PASS: movement") - - # Check look at - driver.execute_script("viewer.lookAt(-10, 90, 100)") - time.sleep(2) - assert driver.execute_script( - "return viewer.getPitch() == -10 && viewer.getYaw() == 90 && viewer.getHfov() == 100" - ) - print("PASS: look at") - - # Check cube - driver.execute_script("viewer.loadScene('cube')") - time.sleep(5) - assert driver.execute_script("return viewer.getScene() == 'cube'") - if CREATE_REF: - viewer.screenshot("tests/cube.png") - subprocess.call(["optipng", "-o7", "-strip", "all", "cube.png"]) - else: - reference = Image.open("tests/cube.png") - screenshot = Image.open(io.BytesIO(viewer.screenshot_as_png)).convert("RGB") - diff = np.mean(np.array(ImageChops.difference(screenshot, reference))) - print("cube difference:", diff) - assert diff < 3 - print("PASS: cube") - - # Check hot spot - driver.find_element_by_class_name("pnlm-scene").click() - time.sleep(5) - assert driver.execute_script("return viewer.getScene() == 'multires'") - print("PASS: hot spot") - - # Check multires - if CREATE_REF: - viewer.screenshot("tests/multires.png") - subprocess.call(["optipng", "-o7", "-strip", "all", "multires.png"]) - else: - reference = Image.open("tests/multires.png") - screenshot = Image.open(io.BytesIO(viewer.screenshot_as_png)).convert("RGB") - diff = np.mean(np.array(ImageChops.difference(screenshot, reference))) - print("multires difference:", diff) - assert diff < 3 - print("PASS: multires") - - -try: - run_tests() -finally: - driver.quit() - httpd.shutdown() - thread.join() +if __name__ == '__main__': + main() diff --git a/utils/build/build.py b/utils/build/build.py index d5e3945..41feac0 100755 --- a/utils/build/build.py +++ b/utils/build/build.py @@ -99,7 +99,11 @@ def build(files, css, html, filename, release=False): if release: version = read('../VERSION').strip() else: - version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').strip() + if os.path.exists('.git'): + version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').strip() + else: + print('No .git folder detected, setting version to testing') + version = "testing" js = js.replace('"_blank">Pannellum','"_blank">Pannellum ' + version) with open('../../src/standalone/standalone.js', 'r') as f: standalone_js = f.read()