Files
galaxy/test/casperjs/casperjs_runner.py
T

468 lines
19 KiB
Python

"""Test runner for casperjs headless browser tests with the Galaxy distribution.
Allows integration of casperjs tests with buildbot, run_functional_tests.sh
Tests can be run in any of the following ways:
* casperjs test mytests.js --url='http://localhost:8080'
* python casperjs_runner.py
* nosetests
* sh run_tests.sh -j
* sh run_tests.sh test/casperjs/casperjs_runner.py
* sh run_tests.sh
Note: that you can enable (lots of) debugging info using cli options:
* casperjs test api-user-tests.js --url='http://localhost:8080' --verbose=true --logLevel=debug
(see casperjs.org for more information)
Note: This works with CasperJS 1.1 and PhantomJS 1.9.2 and these libraries seem to break backward
compatbility a lot.
Note: You can pass in extra data using --data='<some JSON object>'
and it will be available in your script as spaceghost.fixtureData.
Example using a specific user account:
casperjs test api-history-tests.js --url="http://localhost:8080" \
--data='{ "testUser": { "email": "foo@example.com", "password": "123456" } }'
// ...then, in script:
spaceghost.user.loginOrRegisterUser(
spaceghost.fixtureData.testUser.email,
spaceghost.fixtureData.testUser.password );
Example of specifing user and admin user credentials:
casperjs test api-configuration-tests.js --url="http://localhost:8080" \
--admin='{"email": "foo@example.com", "password": "123456" }' \
"""
# -------------------------------------------------------------------- can't do 2.5
import sys
( major, minor, micro, releaselevel, serial ) = sys.version_info
if minor < 6:
msg = 'casperjs requires python 2.6 or newer. Using: %s' % ( sys.version )
try:
# if nose is installed do a skip test
from nose.plugins.skip import SkipTest
raise SkipTest( msg )
except ImportError, i_err:
raise AssertionError( msg )
# --------------------------------------------------------------------
import os
import subprocess
import json
import errno
import re
import unittest
from server_env import TestEnvironment
import logging
logging.basicConfig( stream=sys.stderr, name=__name__ )
log = logging.getLogger( __name__ )
# ==================================================================== MODULE VARS
_PATH_TO_HEADLESS = 'casperjs'
_TODO = """
get data back from js scripts (uploaded files, etc.)
use returned json to output list of failed assertions if code == 2
better way to turn debugging on from the environment
"""
# ====================================================================
class HeadlessJSJavascriptError( Exception ):
"""An error that occurrs in the javascript test file.
"""
pass
class CasperJSTestCase( unittest.TestCase ):
"""Casper tests running in a unittest framework.
"""
# casper uses a lot of escape codes to colorize output - these capture those and allow removal
escape_code_compiled_pattern = None
escape_code_pattern = r'\x1b\[[\d|;]+m'
# info on where to get casper js - shown when the exec can't be found
casper_info = """
CasperJS is a navigation scripting & testing utility for PhantomJS, written in Javascript.
More information is available at: casperjs.org
"""
# debugging flag - set to true to have casperjs tests output with --verbose=true and --logLevel=debug
#debug = True
debug = False
# bit of a hack - this is the beginning of the last string when capserjs --verbose=true --logLevel=debug
# use this to get subprocess to stop waiting for output
casper_done_str = '# Stopping'
# convert js test results to unittest.TestResults
results_adapter = None # CasperJsonToUnittestResultsConverter()
# ---------------------------------------------------------------- run the js script
def run_js_script( self, rel_script_path, *args, **kwargs ):
"""Start the headless browser tests in a separate process and use both
the subprocess return code and the stdout output (formatted as JSON)
to determine which tests failed and which passed.
"""
log.debug( 'beginning headless browser tests: %s', rel_script_path )
process_command_list = self.build_command_line( rel_script_path, *args, **kwargs )
log.debug( 'process_command_list: %s', str( process_command_list ) )
try:
process = subprocess.Popen( process_command_list, shell=False,
stdout=subprocess.PIPE, stderr=subprocess.PIPE )
# output from the browser (stderr only) immediately
while process.poll() == None:
stderr_msg = process.stderr.readline()
stderr_msg = self.strip_escape_codes( stderr_msg.strip() )
if stderr_msg:
log.debug( '(%s): %s', rel_script_path, stderr_msg )
# HACK: this is the last string displayed using the debug settings - afterwards it hangs
# so: bail on this string
if stderr_msg.startswith( self.casper_done_str ):
break
# stdout is assumed to have the json test data/results
( stdout_output, stderr_output ) = process.communicate()
#log.debug( '%s stdout output:\n%s', rel_script_path, stdout_output )
#log.debug( '%s stderr output:\n%s', rel_script_path, stderr_output )
log.debug( 'process.returncode: %d', process.returncode )
# 1.1 has an annoying info bar that happens before it gets to our stuff, so...
stdout_output = '\n'.join( stdout_output.split( '\n' )[1:] )
#log.debug( 'stdout_output:\n' + stdout_output )
if process.returncode == 1:
#TODO: this is a fail on first effect
raise self.browser_error_to_exception( rel_script_path, stdout_output )
# couldn't find the headless browser,
# provide information (as it won't be included by default with galaxy)
except OSError, os_err:
if os_err.errno == errno.ENOENT:
log.error( 'No path to headless browser executable: %s\n'
+ 'These tests were designed to use the following headless browser:\n%s',
self.exec_path, self.casper_info )
raise
return self.handle_js_results( stdout_output )
def build_command_line( self, rel_script_path, *args, **kwargs ):
"""Build the headless browser command line list for subprocess.
"""
command_line_list = [ self.exec_path ]
# as of casperjs 1.1, we always need to use the 'test' command
command_line_list.append( 'test' )
# make rel_script_path an absolute path (when this is not run from its dir - i.e. run_functional_tests.sh)
curr_dir = os.path.dirname( __file__ )
script_path = os.path.join( curr_dir, rel_script_path )
command_line_list.append( script_path )
# let browser know where the server is (from the TestEnvironment created in setUp)
command_line_list.append( '--url=' + self.env.url )
# add the return json only option
# - has script send normal output to stderr and results, errors, logs to stdout as json
command_line_list.append( '--return-json' )
# check flag to output (very) verbose debugging messages from casperjs and tests
#NOTE: this can be set in the class or by using the debug_these_tests flag in server_env
if( ( self.debug )
or ( rel_script_path in self.env.debug_these_tests ) ):
command_line_list.extend([ '--verbose=true', '--logLevel=debug' ])
#TODO: add capture, html output flags
#TODO: allow casperjs cli options ('--includes='), ?in args, kwargs?
command_line_list.extend( args )
# send extra data - encode kwargs as json to pass to casper for decoding
# as of 1.1, we need to pass this under a opt
command_line_list.append( '--data=' + json.dumps( kwargs ) )
return command_line_list
def strip_escape_codes( self, msg ):
"""Removes colorizing escape codes from casper output strings.
"""
if not self.escape_code_compiled_pattern:
self.escape_code_compiled_pattern = re.compile( self.escape_code_pattern )
return re.sub( self.escape_code_compiled_pattern, '', msg )
# ---------------------------------------------------------------- convert js error to python error
def browser_error_to_exception( self, script_path, stdout_output ):
"""Converts the headless' error from JSON into a more informative
python HeadlessJSJavascriptError.
"""
get_error = lambda d: d[ 'errors' ][0]
get_msg = lambda err: err[ 'msg' ]
get_trace = lambda err: err[ 'backtrace' ]
try:
# assume it's json and located in errors (and first)
js_test_results = json.loads( stdout_output )
last_error = get_error( js_test_results )
err_string = ( "%s\n%s" % ( get_msg( last_error ),
self.browser_backtrace_to_string( get_trace( last_error ) ) ) )
# if we couldn't parse json from what's returned on the error, dump stdout
except ValueError, val_err:
if str( val_err ) == 'No JSON object could be decoded':
log.debug( '(error parsing returned JSON from casperjs, dumping stdout...)\n:%s', stdout_output )
return HeadlessJSJavascriptError( 'see log for details' )
else:
raise
# otherwise, raise a vanilla exc
except Exception, exc:
log.debug( '(failed to parse error returned from %s: %s)', _PATH_TO_HEADLESS, str( exc ) )
return HeadlessJSJavascriptError(
"ERROR in headless browser script %s" % ( script_path ) )
# otherwise, raise with msg and backtrace
return HeadlessJSJavascriptError( err_string )
def browser_backtrace_to_string( self, backtrace ):
"""Converts list of trace dictionaries (as might be returned from
json results) to a string similar to a python backtrace.
"""
template = ' File "%s", line %s, in %s'
traces = []
for trace in backtrace:
traces.append( template % ( trace[ 'file' ], trace[ 'line' ], trace[ 'function' ] ) )
return '\n'.join( traces )
# ---------------------------------------------------------------- results
def handle_js_results( self, results ):
"""Handle the results of the js tests by either converting them
with the results adapter or checking for a failure list.
"""
# if given an adapter - use it
if self.results_adapter:
self.results_adapter.convert( results, self )
# - otherwise, assert no failures found
else:
js_test_results = json.loads( results )
failures = js_test_results[ 'failures' ]
assert len( failures ) == 0, (
"%d assertions failed in the headless browser tests" % ( len( failures ) )
+ " (see the log for details)" )
# ---------------------------------------------------------------- TestCase overrides
def setUp( self ):
# set up the env for each test
self.env = TestEnvironment.instance()
self.exec_path = _PATH_TO_HEADLESS
def run( self, result=None ):
# wrap this in order to save ref to result
#TODO: gotta be a better way
self.result = result
unittest.TestCase.run( self, result=result )
# ==================================================================== RESULTS CONVERSION
class CasperJsonToUnittestResultsConverter( object ):
"""Convert casper failures, success to individual unittest.TestResults
"""
#TODO: So far I can add result instances - but each has the id, shortDescription
# of the TestCase.testMethod that called it. Can't find out how to change these.
def convert( self, json_results, test ):
"""Converts JSON test results into unittest.TestResults.
precondition: test should have attribute 'result' which
is a unittest.TestResult (for that test).
"""
results_dict = json.loads( json_results )
failures = results_dict[ 'testResults' ][ 'failures' ]
passes = results_dict[ 'testResults' ][ 'passes' ]
self.add_json_failures_to_results( failures, test )
self.add_json_successes_to_results( passes, test )
def add_json_failures_to_results( self, failures, test ):
"""Converts JSON test failures.
"""
#precondition: result should be an attr of test (a TestResult)
#TODO: no way to change test.desc, name in output?
for failure in failures:
#TODO: doesn't change shortDescription
#if 'standard' in failure:
# self.__doc__ = failure[ 'standard' ]
test.result.addFailure( test, self.casper_failure_to_unittest_failure( failure ) )
test.result.testsRun += 1
def casper_failure_to_unittest_failure( self, casper_failure, failure_class=AssertionError ):
"""Returns a casper test failure (in dictionary form) as a 3-tuple of
the form used by unittest.TestResult.addFailure.
Used to add failures to a casperjs TestCase.
"""
#TODO: this is all too elaborate
fail_type = casper_failure[ 'type' ]
values = json.dumps( casper_failure[ 'values' ] )
desc = casper_failure[ 'standard' ]
if 'messgae' in casper_failure:
desc = casper_failure[ 'message' ]
failure_msg = "(%s) %s: %s" % ( fail_type, desc, values )
#TODO: tb is empty ([]) - can we get file info from casper, covert to py trace?
return ( failure_class, failure_msg, [] )
def add_json_successes_to_results( self, successes, test ):
"""Converts JSON test successes.
"""
for success in successes:
## attempt to re-write test result description - doesn't work
#if 'standard' in success:
# self.__doc__ = success[ 'standard' ]
test.result.addSuccess( test )
test.result.testsRun += 1
# ==================================================================== MODULE FIXTURE
#NOTE: nose will run these automatically
def setup_module():
log.debug( '\n--------------- setting up module' )
def teardown_module():
log.debug( '\n--------------- tearing down module' )
test_user = {
'email': 'test1@test.test',
'password': '123456'
}
# ==================================================================== TESTCASE EXAMPLE
# these could be broken out into other py files - shouldn't be necc. ATM
class Test_01_User( CasperJSTestCase ):
"""Tests for the Galaxy user centered functionality:
registration, login, etc.
"""
def test_10_registration( self ):
"""User registration tests:
register new user, logout, attempt bad registrations.
"""
# all keywords will be compiled into a single JSON obj and passed to the server
#self.run_js_script( 'registration-tests.js',
# testUser=test_user )
# # this causes a time out in history-panel-tests: why?
# # also: I can't seem to bump the timeout to an error (using a handler) - causes script to hang
# # removing for the sake of bbot
self.run_js_script( 'registration-tests.js' )
#TODO:?? could theoretically do db cleanup, checks here with SQLALX
#TODO: have run_js_script return other persistent fixture data (uploaded files, etc.)
def test_20_login( self ):
"""User log in tests.
"""
self.run_js_script( 'login-tests.js' )
class Test_02_Tools( CasperJSTestCase ):
"""(Minimal) casperjs tests for tools.
"""
def test_10_upload( self ):
"""Tests uploading files
"""
self.run_js_script( 'upload-tests.js' )
class Test_03_HistoryPanel( CasperJSTestCase ):
"""Tests for History fetching, rendering, and modeling.
"""
def test_00_history_panel( self ):
"""Test history panel basics (controls, structure, refresh, history options menu, etc.).
"""
self.run_js_script( 'history-panel-tests.js' )
def test_10_history_options( self ):
"""Test history options button.
"""
self.run_js_script( 'history-options-tests.js' )
def test_20_anonymous_histories( self ):
"""Test history panel basics with an anonymous user.
"""
self.run_js_script( 'anon-history-tests.js' )
class Test_04_HDAs( CasperJSTestCase ):
"""Tests for HistoryDatasetAssociation fetching, rendering, and modeling.
"""
def test_00_HDA_states( self ):
"""Test structure rendering of HDAs in all the possible HDA states
"""
self.run_js_script( 'hda-state-tests.js' )
class Test_05_API( CasperJSTestCase ):
"""Tests for API functionality and security.
"""
def test_00_history_api( self ):
"""Test history API.
"""
self.run_js_script( 'api-history-tests.js' )
def test_01_hda_api( self ):
"""Test HDA API.
"""
self.run_js_script( 'api-hda-tests.js' )
def test_02_history_permissions_api( self ):
"""Test API permissions for importable, published histories.
"""
self.run_js_script( 'api-history-permission-tests.js' )
def test_03_anon_history_api( self ):
"""Test API for histories using anonymous user.
"""
self.run_js_script( 'api-anon-history-tests.js' )
def test_04_anon_history_permissions_api( self ):
"""Test API permissions for importable, published histories using anonymous user.
"""
self.run_js_script( 'api-anon-history-permission-tests.js' )
def test_05_workflow_api( self ):
"""Test API for workflows.
"""
self.run_js_script( 'api-workflow-tests.js' )
def test_06_visualization_api( self ):
"""Test API for visualizations.
"""
self.run_js_script( 'api-visualizations-tests.js' )
def test_07_tools_api( self ):
"""Test API for tools.
"""
self.run_js_script( 'api-tool-tests.js' )
def test_08_configuration_api( self ):
"""Test API for configuration.
"""
self.run_js_script( 'api-configuration-tests.js' )
def test_09_user_api( self ):
"""Test API for users.
"""
self.run_js_script( 'api-user-tests.js' )
# ==================================================================== MAIN
if __name__ == '__main__':
log.setLevel( logging.DEBUG )
from server_env import log as server_env_log
server_env_log.setLevel( logging.DEBUG )
setup_module()
#TODO: server_env config doesn't work with unittest's lame main fn
unittest.main()
# teardown_module() isn't called when unittest.main is used