You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

291 lines
9.1 KiB

  1. #!/usr/bin/env python3
  2. """
  3. Selenium-based test suite for Pannellum
  4. Dependencies:
  5. Python 3, Selenium Python bindings, Pillow, NumPy
  6. Either: Firefox & geckodriver or Chrome & chromedriver
  7. Run tests for Pannellum, set up with Continuous Integration.
  8. Contributed by Vanessa Sochat, JOSS Review 2019.
  9. See the project repository for licensing information.
  10. """
  11. from random import choice
  12. from threading import Thread
  13. from http.server import SimpleHTTPRequestHandler
  14. from socketserver import TCPServer
  15. import argparse
  16. import io
  17. import os
  18. import re
  19. import subprocess
  20. import sys
  21. import time
  22. import numpy as np
  23. from PIL import Image, ImageChops
  24. from selenium.common.exceptions import TimeoutException
  25. from selenium import webdriver
  26. class PannellumServer(SimpleHTTPRequestHandler):
  27. """Here we subclass SimpleHTTPServer to capture error messages.
  28. """
  29. def log_message(self, format, *args):
  30. """
  31. Log to standard error with a date time string,
  32. and then call any subclass specific logging functions.
  33. """
  34. sys.stderr.write(
  35. "%s - - [%s] %s\n"
  36. % (self.address_string(), self.log_date_time_string(), format % args)
  37. )
  38. # Workaround for error trying to GET html
  39. if not re.search("div", format % args) and not re.search(
  40. "function", format % args
  41. ):
  42. if re.search("404", format % args):
  43. raise IOError(format % args)
  44. def log_error(self, format, *args):
  45. """Catch errors in the log_messages instead.
  46. """
  47. pass
  48. class PannellumTester(object):
  49. """Bring up a server with a testing robot.
  50. """
  51. def __init__(self, port=None, browser="Chrome", headless=False):
  52. self.handler = PannellumServer
  53. if port:
  54. self.port = port
  55. else:
  56. self.port = choice(range(8000, 9999))
  57. print("Selected port is %s" % self.port)
  58. self.httpd = TCPServer(("", self.port), self.handler)
  59. self.server = Thread(target=self.httpd.serve_forever)
  60. self.server.setDaemon(True)
  61. self.server.start()
  62. self.started = True
  63. self.pause_time = 100
  64. self.browser = None
  65. self.headless = headless
  66. self.display = None
  67. self.driver = browser
  68. def take_screenshot(self, element_id, filename=None):
  69. """Take a screenshot of an element with a given ID.
  70. """
  71. element = self.browser.find_element_by_id(element_id)
  72. img = Image.open(io.BytesIO(element.screenshot_as_png)).convert("RGB")
  73. if filename is not None:
  74. img.save(filename)
  75. return img
  76. def equal_images(self, reference, comparator, name, threshold=5):
  77. """Compare two images, both loaded with PIL, based on pixel differences."""
  78. diff = np.mean(np.array(ImageChops.difference(reference, comparator)))
  79. print("%s difference: %s" % (name, diff))
  80. if diff >= threshold:
  81. comparator.save("tests/" + name + "-comparison.png")
  82. raise ValueError("Screenshot difference is above threshold!")
  83. def run_tests(self, create_ref=False):
  84. """Run tests for Pannellum."""
  85. print("Loading page...")
  86. self.get_page("http://localhost:%s/tests/tests.html" % self.port)
  87. print("Running tests...")
  88. time.sleep(5)
  89. assert self.browser.execute_script("return viewer.isLoaded()") is True
  90. # Check equirectangular
  91. assert self.browser.execute_script(
  92. "return viewer.getScene() == 'equirectangular'"
  93. )
  94. if create_ref:
  95. self.take_screenshot("panorama", "tests/equirectangular.png")
  96. subprocess.call(
  97. ["optipng", "-o7", "-strip", "all", "tests/equirectangular.png"]
  98. )
  99. else:
  100. reference = Image.open("tests/equirectangular.png")
  101. comparator = self.take_screenshot("panorama")
  102. self.equal_images(reference, comparator, "equirectangular")
  103. print("PASS: equirectangular")
  104. # Check movement
  105. self.browser.execute_script("viewer.setPitch(30).setYaw(-20).setHfov(90)")
  106. time.sleep(2)
  107. assert self.browser.execute_script(
  108. "return viewer.getPitch() == 30 && viewer.getYaw() == -20 && viewer.getHfov() == 90"
  109. )
  110. self.browser.find_element_by_class_name("pnlm-zoom-in").click()
  111. time.sleep(1)
  112. assert self.browser.execute_script("return viewer.getHfov() == 85")
  113. self.browser.find_element_by_class_name("pnlm-zoom-out").click()
  114. time.sleep(1)
  115. assert self.browser.execute_script("return viewer.getHfov() == 90")
  116. print("PASS: movement")
  117. # Check look at
  118. self.browser.execute_script("viewer.lookAt(-10, 90, 100)")
  119. time.sleep(2)
  120. assert self.browser.execute_script(
  121. "return viewer.getPitch() == -10 && viewer.getYaw() == 90 && viewer.getHfov() == 100"
  122. )
  123. print("PASS: look at")
  124. # Check cube
  125. self.browser.execute_script("viewer.loadScene('cube')")
  126. time.sleep(5)
  127. assert self.browser.execute_script("return viewer.getScene() == 'cube'")
  128. if create_ref:
  129. self.take_screenshot("panorama", "tests/cube.png")
  130. subprocess.call(["optipng", "-o7", "-strip", "all", "tests/cube.png"])
  131. else:
  132. reference = Image.open("tests/cube.png")
  133. comparator = self.take_screenshot("panorama")
  134. self.equal_images(reference, comparator, "cube")
  135. # Check hot spot
  136. self.browser.find_element_by_class_name("pnlm-scene").click()
  137. time.sleep(5)
  138. assert self.browser.execute_script("return viewer.getScene() == 'multires'")
  139. print("PASS: hot spot")
  140. # Check multires
  141. if create_ref:
  142. self.take_screenshot("panorama", "tests/multires.png")
  143. subprocess.call(["optipng", "-o7", "-strip", "all", "tests/multires.png"])
  144. else:
  145. reference = Image.open("tests/multires.png")
  146. comparator = self.take_screenshot("panorama")
  147. self.equal_images(reference, comparator, "multires")
  148. self.httpd.server_close()
  149. def get_browser(self, name=None):
  150. """Return a browser if it hasn't been initialized yet.
  151. """
  152. if name is None:
  153. name = self.driver
  154. log_path = "tests/%s-driver.log" % name.lower()
  155. if self.browser is None:
  156. if name.lower() == "firefox":
  157. fp = webdriver.FirefoxProfile()
  158. fp.set_preference("layout.css.devPixelsPerPx", "1.0")
  159. self.browser = webdriver.Firefox(
  160. service_log_path=log_path, firefox_profile=fp
  161. )
  162. self.browser.set_window_size(800, 600)
  163. else:
  164. options = webdriver.ChromeOptions()
  165. options.add_argument("headless")
  166. options.add_argument("no-sandbox")
  167. options.add_argument("window-size=800x600")
  168. self.browser = webdriver.Chrome(
  169. service_log_path=log_path, options=options
  170. )
  171. return self.browser
  172. def get_page(self, url):
  173. """Open a particular URL, checking for timeout.
  174. """
  175. if self.browser is None:
  176. self.browser = self.get_browser()
  177. try:
  178. return self.browser.get(url)
  179. except TimeoutException:
  180. print("Browser request timeout. Are you connected to the internet?")
  181. self.browser.close()
  182. sys.exit(1)
  183. def stop(self):
  184. """Close any running browser or server and shut down the robot.
  185. """
  186. if self.browser is not None:
  187. self.browser.close()
  188. self.httpd.server_close()
  189. if self.display is not None:
  190. self.display.close()
  191. def get_parser():
  192. parser = argparse.ArgumentParser(description="Run tests for Pannellum")
  193. parser.add_argument(
  194. "--port",
  195. "-p",
  196. dest="port",
  197. help="Port to run web server",
  198. type=int,
  199. default=None,
  200. )
  201. parser.add_argument(
  202. "--headless",
  203. dest="headless",
  204. help="Start a display before browser",
  205. action="store_true",
  206. default=False,
  207. )
  208. parser.add_argument(
  209. "--create-ref", dest="create_ref", action="store_true", default=False
  210. )
  211. parser.add_argument(
  212. "--browser",
  213. "-b",
  214. dest="browser",
  215. choices=["Firefox", "Chrome"],
  216. help="Browser driver to use for the robot",
  217. type=str,
  218. default="Chrome",
  219. )
  220. return parser
  221. def main():
  222. parser = get_parser()
  223. try:
  224. args = parser.parse_args()
  225. except:
  226. sys.exit(0)
  227. # Add this script's directory, in case it contains driver binaries
  228. here = os.path.abspath(os.path.dirname(__file__))
  229. os.environ["PATH"] = here + ":" + os.environ["PATH"]
  230. os.chdir(here)
  231. # We must be in root directory
  232. os.chdir("..")
  233. # Initialize the tester
  234. tester = PannellumTester(
  235. browser=args.browser, port=args.port, headless=args.headless
  236. )
  237. # Run tests
  238. tester.run_tests(create_ref=args.create_ref)
  239. # Clean up shop!
  240. tester.stop()
  241. if __name__ == "__main__":
  242. main()