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.
 
 
 
 
 
 

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