testing.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. r"""
  2. This module provides some testing functionality for paraview and
  3. vtk web applications. It provides the ability to run an arbitrary
  4. test script in a separate thread and communicate the results back
  5. to the service so that the CTest framework can be notified of the
  6. success or failure of the test.
  7. This test harness will notice when the test script has finished
  8. running and will notify the service to stop. At this point, the
  9. test results will be checked in the main thread which ran the
  10. service, and in the case of failure an exception will be raised
  11. to notify CTest of the failure.
  12. Test scripts need to follow some simple rules in order to work
  13. within the test harness framework:
  14. 1) implement a function called "runTest(args)", where the args
  15. parameter contains all the arguments given to the web application
  16. upon starting. Among other important items, args will contain the
  17. port number where the web application is listening.
  18. 2) import the testing module so that the script has access to
  19. the functions which indicate success and failure. Also the
  20. testing module contains convenience functions that might be of
  21. use to the test scripts.
  22. from vtk.web import testing
  23. 3) Call the "testPass(testName)" or "testFail(testName)" functions
  24. from within the runTest(args) function to indicate to the framework
  25. whether the test passed or failed.
  26. """
  27. import_warning_info = ""
  28. test_module_comm_queue = None
  29. import vtk
  30. # Try standard Python imports
  31. try:
  32. import os, re, time, datetime, threading, imp, inspect, Queue, types, io
  33. except:
  34. import_warning_info += "\nUnable to load at least one basic Python module"
  35. # Image comparison imports
  36. try:
  37. try:
  38. from PIL import Image
  39. except ImportError:
  40. import Image
  41. except:
  42. raise
  43. import base64
  44. import itertools
  45. except:
  46. import_warning_info += (
  47. "\nUnable to load at least one modules necessary for image comparison"
  48. )
  49. # Browser testing imports
  50. try:
  51. import selenium
  52. from selenium import webdriver
  53. except:
  54. import_warning_info += (
  55. "\nUnable to load at least one module necessary for browser tests"
  56. )
  57. # HTTP imports
  58. try:
  59. import requests
  60. except:
  61. import_warning_info += (
  62. "\nUnable to load at least one module necessary for HTTP tests"
  63. )
  64. # Define some infrastructure to support different (or no) browsers
  65. test_module_browsers = ["firefox", "chrome", "internet_explorer", "safari", "nobrowser"]
  66. class TestModuleBrowsers:
  67. firefox, chrome, internet_explorer, safari, nobrowser = range(5)
  68. # =============================================================================
  69. # We can use this exception type to indicate that the test shouldn't actually
  70. # "fail", rather that it was unable to run because some dependencies were not
  71. # met.
  72. # =============================================================================
  73. class DependencyError(Exception):
  74. def __init__(self, value):
  75. self.value = value
  76. def __str__(self):
  77. return repr(self.value)
  78. # =============================================================================
  79. # This class allows usage as a dictionary and an object with named property
  80. # access.
  81. # =============================================================================
  82. class Dictionary(dict):
  83. def __getattribute__(self, attrName):
  84. return self[attrName]
  85. def __setattr__(self, attrName, attrValue):
  86. self[attrName] = attrValue
  87. # =============================================================================
  88. # Checks whether test script supplied, if so, safely imports needed modules
  89. # =============================================================================
  90. def initialize(opts, reactor=None, cleanupMethod=None):
  91. """
  92. This function should be called to initialize the testing module. The first
  93. important thing it does is to store the options for later, since the
  94. startTestThread function will need them. Then it checks the arguments that
  95. were passed into the server to see if a test was actually requested, making
  96. a note of this fact. Then, if a test was required, this function then
  97. checks if all the necessary testing modules were safely imported, printing
  98. a warning if not. If tests were requested and all modules were present,
  99. then this function sets "test_module_do_testing" to True and sets up the
  100. startTestThread function to be called after the reactor is running.
  101. opts: Parsed arguments from the server
  102. reactor: This argument is optional, but is used by server.py to
  103. cause the test thread to be started only after the server itself
  104. has started. If it is not provided, the test thread is launched
  105. immediately.
  106. cleanupMethod: A callback method you would like the test thread
  107. to execute when the test has finished. This is used by server.py
  108. as a way to have the server terminated after the test has finished,
  109. but could be used for other cleanup purposes. This argument is
  110. also optional.
  111. """
  112. global import_warning_info
  113. global testModuleOptions
  114. testModuleOptions = Dictionary()
  115. # Copy the testing options into something we can easily extend
  116. for arg in vars(opts):
  117. optValue = getattr(opts, arg)
  118. testModuleOptions[arg] = optValue
  119. # If we got one, add the cleanup method to the testing options
  120. if cleanupMethod:
  121. testModuleOptions["cleanupMethod"] = cleanupMethod
  122. # Check if a test was actually requested
  123. if (
  124. testModuleOptions.testScriptPath != ""
  125. and testModuleOptions.testScriptPath is not None
  126. ):
  127. # Check if we ran into trouble with any of the testing imports
  128. if import_warning_info != "":
  129. print("WARNING: Some tests may have unmet dependencies")
  130. print(import_warning_info)
  131. if reactor is not None:
  132. # Add startTest callback to the reactor callback queue, so that
  133. # the test thread gets started after the reactor is running. Of
  134. # course this should only happen if everything is good for tests.
  135. reactor.callWhenRunning(_start_test_thread)
  136. else:
  137. # Otherwise, our aim is to start the thread from another process
  138. # so just call the start method.
  139. _start_test_thread()
  140. # =============================================================================
  141. # Grab out the command-line arguments needed for by the testing module.
  142. # =============================================================================
  143. def add_arguments(parser):
  144. """
  145. This function retrieves any command-line arguments that the client-side
  146. tester needs. In order to run a test, you will typically just need the
  147. following:
  148. --run-test-script => This should be the full path to the test script to
  149. be run.
  150. --baseline-img-dir => This should be the 'Baseline' directory where the
  151. baseline images for this test are located.
  152. --test-use-browser => This should be one of the supported browser types,
  153. or else 'nobrowser'. The choices are 'chrome', 'firefox', 'internet_explorer',
  154. 'safari', or 'nobrowser'.
  155. """
  156. parser.add_argument(
  157. "--run-test-script",
  158. default="",
  159. help="The path to a test script to run",
  160. dest="testScriptPath",
  161. )
  162. parser.add_argument(
  163. "--baseline-img-dir",
  164. default="",
  165. help="The path to the directory containing the web test baseline images",
  166. dest="baselineImgDir",
  167. )
  168. parser.add_argument(
  169. "--test-use-browser",
  170. default="nobrowser",
  171. help="One of 'chrome', 'firefox', 'internet_explorer', 'safari', or 'nobrowser'.",
  172. dest="useBrowser",
  173. )
  174. parser.add_argument(
  175. "--temporary-directory",
  176. default=".",
  177. help="A temporary directory for storing test images and diffs",
  178. dest="tmpDirectory",
  179. )
  180. parser.add_argument(
  181. "--test-image-file-name",
  182. default="",
  183. help="Name of file in which to store generated test image",
  184. dest="testImgFile",
  185. )
  186. # =============================================================================
  187. # Initialize the test client
  188. # =============================================================================
  189. def _start_test_thread():
  190. """
  191. This function checks whether testing is required and if so, sets up a Queue
  192. for the purpose of communicating with the thread. then it starts the
  193. after waiting 5 seconds for the server to have a chance to start up.
  194. """
  195. global test_module_comm_queue
  196. test_module_comm_queue = Queue.Queue()
  197. t = threading.Thread(
  198. target=launch_web_test,
  199. args=[],
  200. kwargs={
  201. "serverOpts": testModuleOptions,
  202. "commQueue": test_module_comm_queue,
  203. "testScript": testModuleOptions.testScriptPath,
  204. },
  205. )
  206. t.start()
  207. # =============================================================================
  208. # Test scripts call this function to indicate passage of their test
  209. # =============================================================================
  210. def test_pass(testName):
  211. """
  212. Test scripts should call this function to indicate that the test passed. A
  213. note is recorded that the test succeeded, and is checked later on from the
  214. main thread so that CTest can be notified of this result.
  215. """
  216. global test_module_comm_queue
  217. resultObj = {testName: "pass"}
  218. test_module_comm_queue.put(resultObj)
  219. # =============================================================================
  220. # Test scripts call this function to indicate failure of their test
  221. # =============================================================================
  222. def test_fail(testName):
  223. """
  224. Test scripts should call this function to indicate that the test failed. A
  225. note is recorded that the test did not succeed, and this note is checked
  226. later from the main thread so that CTest can be notified of the result.
  227. The main thread is the only one that can signal test failure in
  228. CTest framework, and the main thread won't have a chance to check for
  229. passage or failure of the test until the main loop has terminated. So
  230. here we just record the failure result, then we check this result in the
  231. processTestResults() function, throwing an exception at that point to
  232. indicate to CTest that the test failed.
  233. """
  234. global test_module_comm_queue
  235. resultObj = {testName: "fail"}
  236. test_module_comm_queue.put(resultObj)
  237. # =============================================================================
  238. # Concatenate any number of strings into a single path string.
  239. # =============================================================================
  240. def concat_paths(*pathElts):
  241. """
  242. A very simple convenience function so that test scripts can build platform
  243. independent paths out of a list of elements, without having to import the
  244. os module.
  245. pathElts: Any number of strings which should be concatenated together
  246. in a platform independent manner.
  247. """
  248. return os.path.join(*pathElts)
  249. # =============================================================================
  250. # So we can change our time format in a single place, this function is
  251. # provided.
  252. # =============================================================================
  253. def get_current_time_string():
  254. """
  255. This function returns the current time as a string, using ISO 8601 format.
  256. """
  257. return datetime.datetime.now().isoformat(" ")
  258. # =============================================================================
  259. # Uses vtkTesting to compare images. According to comments in the vtkTesting
  260. # C++ code (and this seems to work), if there are multiple baseline images in
  261. # the same directory as the baseline_img, and they follow the naming pattern:
  262. # 'img.png', 'img_1.png', ... , 'img_N.png', then all of these images will be
  263. # tried for a match.
  264. # =============================================================================
  265. def compare_images(test_img, baseline_img, tmp_dir="."):
  266. """
  267. This function creates a vtkTesting object, and specifies the name of the
  268. baseline image file, using a fully qualified path (baseline_img must be
  269. fully qualified). Then it calls the vtkTesting method which compares the
  270. image (test_img, specified only with a relative path) against the baseline
  271. image as well as any other images in the same directory as the baseline
  272. image which follow the naming pattern: 'img.png', 'img_1.png', ... , 'img_N.png'
  273. test_img: File name of output image to be compared against baseline.
  274. baseline_img: Fully qualified path to first of the baseline images.
  275. tmp_dir: Fully qualified path to a temporary directory for storing images.
  276. """
  277. # Create a vtkTesting object and specify a baseline image
  278. t = vtk.vtkTesting()
  279. t.AddArgument("-T")
  280. t.AddArgument(tmp_dir)
  281. t.AddArgument("-V")
  282. t.AddArgument(baseline_img)
  283. # Perform the image comparison test and print out the result.
  284. return t.RegressionTest(test_img, 0.0)
  285. # =============================================================================
  286. # Provide a wait function
  287. # =============================================================================
  288. def wait_with_timeout(delay=None, limit=0, criterion=None):
  289. """
  290. This function provides the ability to wait for a certain number of seconds,
  291. or else to wait for a specific criterion to be met.
  292. """
  293. for i in itertools.count():
  294. if criterion is not None and criterion():
  295. return True
  296. elif delay * i > limit:
  297. return False
  298. else:
  299. time.sleep(delay)
  300. # =============================================================================
  301. # Define a WebTest class with five stages of testing: initialization, setup,
  302. # capture, postprocess, and cleanup.
  303. # =============================================================================
  304. class WebTest(object):
  305. """
  306. This is the base class for all automated web-based tests. It defines five
  307. stages that any test must run through, and allows any or all of these
  308. stages to be overridden by subclasses. This class defines the run_test
  309. method to invoke the five stages overridden by subclasses, one at a time:
  310. 1) initialize, 2) setup, 3) capture, 4) postprocess, and 5) cleanup.
  311. """
  312. class Abort:
  313. pass
  314. def __init__(self, url=None, testname=None, **kwargs):
  315. self.url = url
  316. self.testname = testname
  317. def run_test(self):
  318. try:
  319. self.checkdependencies()
  320. self.initialize()
  321. self.setup()
  322. self.capture()
  323. self.postprocess()
  324. except WebTest.Abort:
  325. # Placeholder for future option to return failure result
  326. pass
  327. except:
  328. self.cleanup()
  329. raise
  330. self.cleanup()
  331. def checkdependencies(self):
  332. pass
  333. def initialize(self):
  334. pass
  335. def setup(self):
  336. pass
  337. def capture(self):
  338. pass
  339. def postprocess(self):
  340. pass
  341. def cleanup(self):
  342. pass
  343. # =============================================================================
  344. # Define a WebTest subclass designed specifically for browser-based tests.
  345. # =============================================================================
  346. class BrowserBasedWebTest(WebTest):
  347. """
  348. This class can be used as a base for any browser-based web tests. It
  349. introduces the notion of a selenium browser and overrides phases (1) and
  350. (3), initialization and cleanup, of the test phases introduced in the base
  351. class. Initialization involves selecting the browser type, setting the
  352. browser window size, and asking the browser to load the url. Cleanup
  353. involves closing the browser window.
  354. """
  355. def __init__(self, size=None, browser=None, **kwargs):
  356. self.size = size
  357. self.browser = browser
  358. self.window = None
  359. WebTest.__init__(self, **kwargs)
  360. def initialize(self):
  361. try:
  362. if self.browser is None or self.browser == TestModuleBrowsers.chrome:
  363. self.window = webdriver.Chrome()
  364. elif self.browser == TestModuleBrowsers.firefox:
  365. self.window = webdriver.Firefox()
  366. elif self.browser == TestModuleBrowsers.internet_explorer:
  367. self.window = webdriver.Ie()
  368. else:
  369. raise DependencyError(
  370. "self.browser argument has illegal value %r" % (self.browser)
  371. )
  372. except DependencyError as dErr:
  373. raise
  374. except Exception as inst:
  375. raise DependencyError(inst)
  376. if self.size is not None:
  377. self.window.set_window_size(self.size[0], self.size[1])
  378. if self.url is not None:
  379. self.window.get(self.url)
  380. def cleanup(self):
  381. try:
  382. self.window.quit()
  383. except:
  384. print(
  385. "Unable to call window.quit, perhaps this is expected because of unmet browser dependency."
  386. )
  387. # =============================================================================
  388. # Extend BrowserBasedWebTest to handle vtk-style image comparison
  389. # =============================================================================
  390. class ImageComparatorWebTest(BrowserBasedWebTest):
  391. """
  392. This class extends browser based web tests to include image comparison. It
  393. overrides the capture phase of testing with some functionality to simply
  394. grab a screenshot of the entire browser window. It overrides the
  395. postprocess phase with a call to vtk image comparison functionality.
  396. Derived classes can then simply override the setup function with a series
  397. of selenium-based browser interactions to create a complete test. Derived
  398. classes may also prefer to override the capture phase to capture only
  399. certain portions of the browser window for image comparison.
  400. """
  401. def __init__(self, filename=None, baseline=None, temporaryDir=None, **kwargs):
  402. if filename is None:
  403. raise TypeError("missing argument 'filename'")
  404. if baseline is None:
  405. raise TypeError("missing argument 'baseline'")
  406. BrowserBasedWebTest.__init__(self, **kwargs)
  407. self.filename = filename
  408. self.baseline = baseline
  409. self.tmpDir = temporaryDir
  410. def capture(self):
  411. self.window.save_screenshot(self.filename)
  412. def postprocess(self):
  413. result = compare_images(self.filename, self.baseline, self.tmpDir)
  414. if result == 1:
  415. test_pass(self.testname)
  416. else:
  417. test_fail(self.testname)
  418. # =============================================================================
  419. # Given a css selector to use in finding the image element, get the element,
  420. # then base64 decode the "src" attribute and return it.
  421. # =============================================================================
  422. def get_image_data(browser, cssSelector):
  423. """
  424. This function takes a selenium browser and a css selector string and uses
  425. them to find the target HTML image element. The desired image element
  426. should contain it's image data as a Base64 encoded JPEG image string.
  427. The 'src' attribute of the image is read, Base64-decoded, and then
  428. returned.
  429. browser: A selenium browser instance, as created by webdriver.Chrome(),
  430. for example.
  431. cssSelector: A string containing a CSS selector which will be used to
  432. find the HTML image element of interest.
  433. """
  434. # Here's maybe a better way to get at that image element
  435. imageElt = browser.find_element_by_css_selector(cssSelector)
  436. # Now get the Base64 image string and decode it into image data
  437. base64String = imageElt.get_attribute("src")
  438. b64RegEx = re.compile(r"data:image/jpeg;base64,(.+)")
  439. b64Matcher = b64RegEx.match(base64String)
  440. imgdata = base64.b64decode(b64Matcher.group(1))
  441. return imgdata
  442. # =============================================================================
  443. # Combines a variation on above function with the write_image_to_disk function.
  444. # converting jpg to png in the process, if necessary.
  445. # =============================================================================
  446. def save_image_data_as_png(browser, cssSelector, imgfilename):
  447. """
  448. This function takes a selenium browser instance, a css selector string,
  449. and a file name. It uses the css selector string to finds the target HTML
  450. Image element, which should contain a Base64 encoded JPEG image string,
  451. it decodes the string to image data, and then saves the data to the file.
  452. The image type of the written file is determined from the extension of the
  453. provided filename.
  454. browser: A selenium browser instance as created by webdriver.Chrome(),
  455. for example.
  456. cssSelector: A string containing a CSS selector which will be used to
  457. find the HTML image element of interest.
  458. imgFilename: The filename to which to save the image. The extension is
  459. used to determine the type of image which should be saved.
  460. """
  461. imageElt = browser.find_element_by_css_selector(cssSelector)
  462. base64String = imageElt.get_attribute("src")
  463. b64RegEx = re.compile(r"data:image/jpeg;base64,(.+)")
  464. b64Matcher = b64RegEx.match(base64String)
  465. img = Image.open(io.BytesIO(base64.b64decode(b64Matcher.group(1))))
  466. img.save(imgfilename)
  467. # =============================================================================
  468. # Given a decoded image and the full path to a file, write the image to the
  469. # file.
  470. # =============================================================================
  471. def write_image_to_disk(imgData, filePath):
  472. """
  473. This function takes an image data, as returned by this module's
  474. get_image_data() function for example, and writes it out to the file given by
  475. the filePath parameter.
  476. imgData: An image data object
  477. filePath: The full path, including the file name and extension, where
  478. the image should be written.
  479. """
  480. with open(filePath, "wb") as f:
  481. f.write(imgData)
  482. # =============================================================================
  483. # There could be problems if the script file has more than one class defn which
  484. # is a subclass of vtk.web.testing.WebTest, so we should write some
  485. # documentation to help people avoid that.
  486. # =============================================================================
  487. def instantiate_test_subclass(pathToScript, **kwargs):
  488. """
  489. This function takes the fully qualified path to a test file, along with
  490. any needed keyword arguments, then dynamically loads the file as a module
  491. and finds the test class defined inside of it via inspection. It then
  492. uses the keyword arguments to instantiate the test class and return the
  493. instance.
  494. pathToScript: Fully qualified path to python file containing defined
  495. subclass of one of the test base classes.
  496. kwargs: Keyword arguments to be passed to the constructor of the
  497. testing subclass.
  498. """
  499. # Load the file as a module
  500. moduleName = imp.load_source("dynamicTestModule", pathToScript)
  501. instance = None
  502. # Inspect dynamically loaded module members
  503. for name, obj in inspect.getmembers(moduleName):
  504. # Looking for classes only
  505. if inspect.isclass(obj):
  506. instance = obj.__new__(obj)
  507. # And only classes defined in the dynamically loaded module
  508. if instance.__module__ == "dynamicTestModule":
  509. try:
  510. instance.__init__(**kwargs)
  511. break
  512. except Exception as inst:
  513. print("Caught exception: " + str(type(inst)))
  514. print(inst)
  515. raise
  516. return instance
  517. # =============================================================================
  518. # For testing purposes, define a function which can interact with a running
  519. # paraview or vtk web application service.
  520. # =============================================================================
  521. def launch_web_test(*args, **kwargs):
  522. """
  523. This function loads a python file as a module (with no package), and then
  524. instantiates the class it must contain, and finally executes the run_test()
  525. method of the class (which the class may override, but which is defined in
  526. both of the testing base classes, WebTest and ImageComparatorBaseClass).
  527. After the run_test() method finishes, this function will stop the web
  528. server if required. This function expects some keyword arguments will be
  529. present in order for it to complete it's task:
  530. kwargs['serverOpts']: An object containing all the parameters used
  531. to start the web service. Some of them will be used in the test script
  532. in order perform the test. For example, the port on which the server
  533. was started will be required in order to connect to the server.
  534. kwargs['testScript']: The full path to the python file containing the
  535. testing subclass.
  536. """
  537. serverOpts = None
  538. testScriptFile = None
  539. # This is really the thing all test scripts will need: access to all
  540. # the options used to start the server process.
  541. if "serverOpts" in kwargs:
  542. serverOpts = kwargs["serverOpts"]
  543. # print 'These are the serverOpts we got: '
  544. # print serverOpts
  545. # Get the full path to the test script
  546. if "testScript" in kwargs:
  547. testScriptFile = kwargs["testScript"]
  548. testName = "unknown"
  549. # Check for a test file (python file)
  550. if testScriptFile is None:
  551. print("No test script file found, no test script will be run.")
  552. test_fail(testName)
  553. # The test name will be generated from the python script name, so
  554. # match and capture a bunch of contiguous characters which are
  555. # not '.', '\', or '/', followed immediately by the string '.py'.
  556. fnamePattern = re.compile("([^\.\/\\\]+)\.py")
  557. fmatch = re.search(fnamePattern, testScriptFile)
  558. if fmatch:
  559. testName = fmatch.group(1)
  560. else:
  561. print(
  562. "Unable to parse testScriptFile ("
  563. + str(testScriptfile)
  564. + "), no test will be run"
  565. )
  566. test_fail(testName)
  567. # If we successfully got a test name, we are ready to try and run the test
  568. if testName != "unknown":
  569. # Output file and baseline file names are generated from the test name
  570. imgFileName = testName + ".png"
  571. knownGoodFileName = concat_paths(serverOpts.baselineImgDir, imgFileName)
  572. tempDir = serverOpts.tmpDirectory
  573. testImgFileName = serverOpts.testImgFile
  574. testBrowser = test_module_browsers.index(serverOpts.useBrowser)
  575. # Now try to instantiate and run the test
  576. try:
  577. testInstance = instantiate_test_subclass(
  578. testScriptFile,
  579. testname=testName,
  580. host=serverOpts.host,
  581. port=serverOpts.port,
  582. browser=testBrowser,
  583. filename=testImgFileName,
  584. baseline=knownGoodFileName,
  585. temporaryDir=tempDir,
  586. )
  587. # If we were able to instantiate the test, run it, otherwise we
  588. # consider it a failure.
  589. if testInstance is not None:
  590. try:
  591. testInstance.run_test()
  592. except DependencyError as derr:
  593. # TODO: trigger return SKIP_RETURN_CODE when CMake 3 is required
  594. print(
  595. "Some dependency of this test was not met, allowing it to pass"
  596. )
  597. test_pass(testName)
  598. else:
  599. print("Unable to instantiate test instance, failing test")
  600. test_fail(testName)
  601. return
  602. except Exception as inst:
  603. import sys, traceback
  604. tb = sys.exc_info()[2]
  605. print("Caught an exception while running test script:")
  606. print(" " + str(type(inst)))
  607. print(" " + str(inst))
  608. print(" " + "".join(traceback.format_tb(tb)))
  609. test_fail(testName)
  610. # If we were passed a cleanup method to run after testing, invoke it now
  611. if "cleanupMethod" in serverOpts:
  612. serverOpts["cleanupMethod"]()
  613. # =============================================================================
  614. # To keep the service module clean, we'll process the test results here, given
  615. # the test result object we generated in "launch_web_test". It is
  616. # passed back to this function after the service has completed. Failure of
  617. # of the test is indicated by raising an exception in here.
  618. # =============================================================================
  619. def finalize():
  620. """
  621. This function checks the module's global test_module_comm_queue variable for a
  622. test result. If one is found and the result is 'fail', then this function
  623. raises an exception to communicate the failure to the CTest framework.
  624. In order for a test result to be found in the test_module_comm_queue variable,
  625. the test script must have called either the testPass or testFail functions
  626. provided by this test module before returning.
  627. """
  628. global test_module_comm_queue
  629. if test_module_comm_queue is not None:
  630. resultObject = test_module_comm_queue.get()
  631. failedATest = False
  632. for testName in resultObject:
  633. testResult = resultObject[testName]
  634. if testResult == "fail":
  635. print(" Test -> " + testName + ": " + testResult)
  636. failedATest = True
  637. if failedATest is True:
  638. raise Exception(
  639. "At least one of the requested tests failed. "
  640. + "See detailed output, above, for more information"
  641. )