/test/base/twilltestcase.py

https://bitbucket.org/cistrome/cistrome-harvard/ · Python · 2600 lines · 2502 code · 18 blank · 80 comment · 136 complexity · df943a8c086a1e8b21a50a6cb0a60990 MD5 · raw file

Large files are truncated click here to view the full file

  1. import difflib
  2. import filecmp
  3. import logging
  4. import os
  5. import re
  6. import pprint
  7. import shutil
  8. import StringIO
  9. import subprocess
  10. import tarfile
  11. import tempfile
  12. import time
  13. import unittest
  14. import urllib
  15. import zipfile
  16. from galaxy.web import security
  17. from galaxy.web.framework.helpers import iff
  18. from galaxy.util.json import from_json_string
  19. from base.asserts import verify_assertions
  20. from galaxy import eggs
  21. eggs.require( "elementtree" )
  22. eggs.require( 'twill' )
  23. from elementtree import ElementTree
  24. import twill
  25. import twill.commands as tc
  26. from twill.other_packages._mechanize_dist import ClientForm
  27. #Force twill to log to a buffer -- FIXME: Should this go to stdout and be captured by nose?
  28. buffer = StringIO.StringIO()
  29. twill.set_output( buffer )
  30. tc.config( 'use_tidy', 0 )
  31. # Dial ClientCookie logging down (very noisy)
  32. logging.getLogger( "ClientCookie.cookies" ).setLevel( logging.WARNING )
  33. log = logging.getLogger( __name__ )
  34. class TwillTestCase( unittest.TestCase ):
  35. def setUp( self ):
  36. # Security helper
  37. self.security = security.SecurityHelper( id_secret='changethisinproductiontoo' )
  38. self.history_id = os.environ.get( 'GALAXY_TEST_HISTORY_ID', None )
  39. self.host = os.environ.get( 'GALAXY_TEST_HOST' )
  40. self.port = os.environ.get( 'GALAXY_TEST_PORT' )
  41. self.url = "http://%s:%s" % ( self.host, self.port )
  42. self.file_dir = os.environ.get( 'GALAXY_TEST_FILE_DIR', None )
  43. self.tool_shed_test_file = os.environ.get( 'GALAXY_TOOL_SHED_TEST_FILE', None )
  44. if self.tool_shed_test_file:
  45. f = open( self.tool_shed_test_file, 'r' )
  46. text = f.read()
  47. f.close()
  48. self.shed_tools_dict = from_json_string( text )
  49. else:
  50. self.shed_tools_dict = {}
  51. self.keepOutdir = os.environ.get( 'GALAXY_TEST_SAVE', '' )
  52. if self.keepOutdir > '':
  53. try:
  54. os.makedirs(self.keepOutdir)
  55. except:
  56. pass
  57. self.home()
  58. # Functions associated with files
  59. def files_diff( self, file1, file2, attributes=None ):
  60. """Checks the contents of 2 files for differences"""
  61. def get_lines_diff( diff ):
  62. count = 0
  63. for line in diff:
  64. if ( line.startswith( '+' ) and not line.startswith( '+++' ) ) or ( line.startswith( '-' ) and not line.startswith( '---' ) ):
  65. count += 1
  66. return count
  67. if not filecmp.cmp( file1, file2 ):
  68. files_differ = False
  69. local_file = open( file1, 'U' ).readlines()
  70. history_data = open( file2, 'U' ).readlines()
  71. if attributes is None:
  72. attributes = {}
  73. if attributes.get( 'sort', False ):
  74. history_data.sort()
  75. ##Why even bother with the check loop below, why not just use the diff output? This seems wasteful.
  76. if len( local_file ) == len( history_data ):
  77. for i in range( len( history_data ) ):
  78. if local_file[i].rstrip( '\r\n' ) != history_data[i].rstrip( '\r\n' ):
  79. files_differ = True
  80. break
  81. else:
  82. files_differ = True
  83. if files_differ:
  84. allowed_diff_count = int(attributes.get( 'lines_diff', 0 ))
  85. diff = list( difflib.unified_diff( local_file, history_data, "local_file", "history_data" ) )
  86. diff_lines = get_lines_diff( diff )
  87. if diff_lines > allowed_diff_count:
  88. if len(diff) < 60:
  89. diff_slice = diff[0:40]
  90. else:
  91. diff_slice = diff[:25] + ["********\n", "*SNIP *\n", "********\n"] + diff[-25:]
  92. #FIXME: This pdf stuff is rather special cased and has not been updated to consider lines_diff
  93. #due to unknown desired behavior when used in conjunction with a non-zero lines_diff
  94. #PDF forgiveness can probably be handled better by not special casing by __extension__ here
  95. #and instead using lines_diff or a regular expression matching
  96. #or by creating and using a specialized pdf comparison function
  97. if file1.endswith( '.pdf' ) or file2.endswith( '.pdf' ):
  98. # PDF files contain creation dates, modification dates, ids and descriptions that change with each
  99. # new file, so we need to handle these differences. As long as the rest of the PDF file does
  100. # not differ we're ok.
  101. valid_diff_strs = [ 'description', 'createdate', 'creationdate', 'moddate', 'id', 'producer', 'creator' ]
  102. valid_diff = False
  103. invalid_diff_lines = 0
  104. for line in diff_slice:
  105. # Make sure to lower case strings before checking.
  106. line = line.lower()
  107. # Diff lines will always start with a + or - character, but handle special cases: '--- local_file \n', '+++ history_data \n'
  108. if ( line.startswith( '+' ) or line.startswith( '-' ) ) and line.find( 'local_file' ) < 0 and line.find( 'history_data' ) < 0:
  109. for vdf in valid_diff_strs:
  110. if line.find( vdf ) < 0:
  111. valid_diff = False
  112. else:
  113. valid_diff = True
  114. # Stop checking as soon as we know we have a valid difference
  115. break
  116. if not valid_diff:
  117. invalid_diff_lines += 1
  118. log.info('## files diff on %s and %s lines_diff=%d, found diff = %d, found pdf invalid diff = %d' % (file1, file2, allowed_diff_count, diff_lines, invalid_diff_lines))
  119. if invalid_diff_lines > allowed_diff_count:
  120. # Print out diff_slice so we can see what failed
  121. print "###### diff_slice ######"
  122. raise AssertionError( "".join( diff_slice ) )
  123. else:
  124. log.info('## files diff on %s and %s lines_diff=%d, found diff = %d' % (file1, file2, allowed_diff_count, diff_lines))
  125. for line in diff_slice:
  126. for char in line:
  127. if ord( char ) > 128:
  128. raise AssertionError( "Binary data detected, not displaying diff" )
  129. raise AssertionError( "".join( diff_slice ) )
  130. def files_re_match( self, file1, file2, attributes=None ):
  131. """Checks the contents of 2 files for differences using re.match"""
  132. local_file = open( file1, 'U' ).readlines() # regex file
  133. history_data = open( file2, 'U' ).readlines()
  134. assert len( local_file ) == len( history_data ), 'Data File and Regular Expression File contain a different number of lines (%s != %s)\nHistory Data (first 40 lines):\n%s' % ( len( local_file ), len( history_data ), ''.join( history_data[:40] ) )
  135. if attributes is None:
  136. attributes = {}
  137. if attributes.get( 'sort', False ):
  138. history_data.sort()
  139. lines_diff = int(attributes.get( 'lines_diff', 0 ))
  140. line_diff_count = 0
  141. diffs = []
  142. for i in range( len( history_data ) ):
  143. if not re.match( local_file[i].rstrip( '\r\n' ), history_data[i].rstrip( '\r\n' ) ):
  144. line_diff_count += 1
  145. diffs.append( 'Regular Expression: %s\nData file : %s' % ( local_file[i].rstrip( '\r\n' ), history_data[i].rstrip( '\r\n' ) ) )
  146. if line_diff_count > lines_diff:
  147. raise AssertionError( "Regular expression did not match data file (allowed variants=%i):\n%s" % ( lines_diff, "".join( diffs ) ) )
  148. def files_re_match_multiline( self, file1, file2, attributes=None ):
  149. """Checks the contents of 2 files for differences using re.match in multiline mode"""
  150. local_file = open( file1, 'U' ).read() # regex file
  151. if attributes is None:
  152. attributes = {}
  153. if attributes.get( 'sort', False ):
  154. history_data = open( file2, 'U' ).readlines()
  155. history_data.sort()
  156. history_data = ''.join( history_data )
  157. else:
  158. history_data = open( file2, 'U' ).read()
  159. #lines_diff not applicable to multiline matching
  160. assert re.match( local_file, history_data, re.MULTILINE ), "Multiline Regular expression did not match data file"
  161. def files_contains( self, file1, file2, attributes=None ):
  162. """Checks the contents of file2 for substrings found in file1, on a per-line basis"""
  163. local_file = open( file1, 'U' ).readlines() # regex file
  164. #TODO: allow forcing ordering of contains
  165. history_data = open( file2, 'U' ).read()
  166. lines_diff = int( attributes.get( 'lines_diff', 0 ) )
  167. line_diff_count = 0
  168. while local_file:
  169. contains = local_file.pop( 0 ).rstrip( '\n\r' )
  170. if contains not in history_data:
  171. line_diff_count += 1
  172. if line_diff_count > lines_diff:
  173. raise AssertionError( "Failed to find '%s' in history data. (lines_diff=%i):\n" % ( contains, lines_diff ) )
  174. def get_filename( self, filename, shed_tool_id=None ):
  175. if shed_tool_id and self.shed_tools_dict:
  176. file_dir = self.shed_tools_dict[ shed_tool_id ]
  177. if not file_dir:
  178. file_dir = self.file_dir
  179. else:
  180. file_dir = self.file_dir
  181. return os.path.abspath( os.path.join( file_dir, filename ) )
  182. def save_log( *path ):
  183. """Saves the log to a file"""
  184. filename = os.path.join( *path )
  185. file(filename, 'wt').write(buffer.getvalue())
  186. def upload_file( self, filename, ftype='auto', dbkey='unspecified (?)', space_to_tab=False, metadata=None, composite_data=None, name=None, shed_tool_id=None, wait=True ):
  187. """
  188. Uploads a file. If shed_tool_id has a value, we're testing tools migrated from the distribution to the tool shed,
  189. so the tool-data directory of test data files is contained in the installed tool shed repository.
  190. """
  191. self.visit_url( "%s/tool_runner?tool_id=upload1" % self.url )
  192. try:
  193. self.refresh_form( "file_type", ftype ) # Refresh, to support composite files
  194. tc.fv( "tool_form", "dbkey", dbkey )
  195. if metadata:
  196. for elem in metadata:
  197. tc.fv( "tool_form", "files_metadata|%s" % elem.get( 'name' ), elem.get( 'value' ) )
  198. if composite_data:
  199. for i, composite_file in enumerate( composite_data ):
  200. filename = self.get_filename( composite_file.get( 'value' ), shed_tool_id=shed_tool_id )
  201. tc.formfile( "tool_form", "files_%i|file_data" % i, filename )
  202. tc.fv( "tool_form", "files_%i|space_to_tab" % i, composite_file.get( 'space_to_tab', False ) )
  203. else:
  204. filename = self.get_filename( filename, shed_tool_id=shed_tool_id )
  205. tc.formfile( "tool_form", "file_data", filename )
  206. tc.fv( "tool_form", "space_to_tab", space_to_tab )
  207. if name:
  208. # NAME is a hidden form element, so the following prop must
  209. # set to use it.
  210. tc.config("readonly_controls_writeable", 1)
  211. tc.fv( "tool_form", "NAME", name )
  212. tc.submit( "runtool_btn" )
  213. self.home()
  214. except AssertionError, err:
  215. errmsg = "Uploading file resulted in the following exception. Make sure the file (%s) exists. " % filename
  216. errmsg += str( err )
  217. raise AssertionError( errmsg )
  218. if not wait:
  219. return
  220. # Make sure every history item has a valid hid
  221. hids = self.get_hids_in_history()
  222. for hid in hids:
  223. try:
  224. int( hid )
  225. except:
  226. raise AssertionError( "Invalid hid (%s) created when uploading file %s" % ( hid, filename ) )
  227. # Wait for upload processing to finish (TODO: this should be done in each test case instead)
  228. self.wait()
  229. def upload_url_paste( self, url_paste, ftype='auto', dbkey='unspecified (?)' ):
  230. """Pasted data in the upload utility"""
  231. self.visit_page( "tool_runner/index?tool_id=upload1" )
  232. try:
  233. self.refresh_form( "file_type", ftype ) #Refresh, to support composite files
  234. tc.fv( "tool_form", "dbkey", dbkey )
  235. tc.fv( "tool_form", "url_paste", url_paste )
  236. tc.submit( "runtool_btn" )
  237. self.home()
  238. except Exception, e:
  239. errmsg = "Problem executing upload utility using url_paste: %s" % str( e )
  240. raise AssertionError( errmsg )
  241. # Make sure every history item has a valid hid
  242. hids = self.get_hids_in_history()
  243. for hid in hids:
  244. try:
  245. int( hid )
  246. except:
  247. raise AssertionError( "Invalid hid (%s) created when pasting %s" % ( hid, url_paste ) )
  248. # Wait for upload processing to finish (TODO: this should be done in each test case instead)
  249. self.wait()
  250. def json_from_url( self, url ):
  251. self.visit_url( url )
  252. return from_json_string( self.last_page() )
  253. # Functions associated with histories
  254. def get_history_from_api( self, encoded_history_id=None ):
  255. if encoded_history_id is None:
  256. history = self.get_latest_history()
  257. encoded_history_id = history[ 'id' ]
  258. return self.json_from_url( '/api/histories/%s/contents' % encoded_history_id )
  259. def get_latest_history( self ):
  260. return self.json_from_url( '/api/histories' )[ 0 ]
  261. def find_hda_by_dataset_name( self, name, history=None ):
  262. if history is None:
  263. history = self.get_history_from_api()
  264. for hda in history:
  265. if hda[ 'name' ] == name:
  266. return hda
  267. def check_history_for_errors( self ):
  268. """Raises an exception if there are errors in a history"""
  269. self.home()
  270. self.visit_page( "history" )
  271. page = self.last_page()
  272. if page.find( 'error' ) > -1:
  273. raise AssertionError( 'Errors in the history for user %s' % self.user )
  274. def check_history_for_string( self, patt, show_deleted=False ):
  275. """Breaks patt on whitespace and searches for each element seperately in the history"""
  276. self.home()
  277. if show_deleted:
  278. self.visit_page( "history?show_deleted=True" )
  279. else:
  280. self.visit_page( "history" )
  281. for subpatt in patt.split():
  282. try:
  283. tc.find( subpatt )
  284. except:
  285. fname = self.write_temp_file( tc.browser.get_html() )
  286. errmsg = "no match to '%s'\npage content written to '%s'" % ( subpatt, fname )
  287. raise AssertionError( errmsg )
  288. self.home()
  289. def check_history_for_exact_string( self, string, show_deleted=False ):
  290. """Looks for exact match to 'string' in history page"""
  291. self.home()
  292. if show_deleted:
  293. self.visit_page( "history?show_deleted=True" )
  294. else:
  295. self.visit_page( "history" )
  296. try:
  297. tc.find( string )
  298. except:
  299. fname = self.write_temp_file( tc.browser.get_html() )
  300. errmsg = "no match to '%s'\npage content written to '%s'" % ( string, fname )
  301. raise AssertionError( errmsg )
  302. self.home()
  303. def check_history_json( self, pattern, check_fn, show_deleted=None, multiline=True ):
  304. """
  305. Tries to find a JSON string in the history page using the regex pattern,
  306. parse it, and assert check_fn returns True when called on that parsed
  307. data.
  308. """
  309. self.home()
  310. if show_deleted:
  311. self.visit_page( "history?show_deleted=True" )
  312. elif show_deleted == False:
  313. self.visit_page( "history?show_deleted=False" )
  314. else:
  315. self.visit_page( "history" )
  316. json_data = {}
  317. try:
  318. tc.find( pattern, flags=( 'm' if multiline else '' ) )
  319. # twill stores the regex match in a special stack variable
  320. match = twill.namespaces.get_twill_glocals()[1][ '__match__' ]
  321. json_data = from_json_string( match )
  322. assert check_fn( json_data ), 'failed check_fn: %s' % ( check_fn.func_name )
  323. except Exception, exc:
  324. log.error( exc, exc_info=True )
  325. log.debug( 'json_data: %s', ( '\n' + pprint.pformat( json_data ) if json_data else '(no match)' ) )
  326. fname = self.write_temp_file( tc.browser.get_html() )
  327. errmsg = ( "json '%s' could not be found or failed check_fn" % ( pattern ) +
  328. "\npage content written to '%s'" % ( fname ) )
  329. raise AssertionError( errmsg )
  330. self.home()
  331. def is_history_empty( self ):
  332. """
  333. Uses history page JSON to determine whether this history is empty
  334. (i.e. has no undeleted datasets).
  335. """
  336. return len( self.get_history_from_api() ) == 0
  337. def check_hda_json_for_key_value( self, hda_id, key, value, use_string_contains=False ):
  338. """
  339. Uses the history API to determine whether the current history:
  340. (1) Has a history dataset with the required ID.
  341. (2) That dataset has the required key.
  342. (3) The contents of that key match the provided value.
  343. If use_string_contains=True, this will perform a substring match, otherwise an exact match.
  344. """
  345. #TODO: multi key, value
  346. hda = dict()
  347. for history_item in self.get_history_from_api():
  348. if history_item[ 'id' ] == hda_id:
  349. hda = self.json_from_url( history_item[ 'url' ] )
  350. break
  351. if hda:
  352. if key in hda:
  353. if use_string_contains:
  354. return value in hda[ key ]
  355. else:
  356. return value == hda[ key ]
  357. return False
  358. def clear_history( self ):
  359. """Empties a history of all datasets"""
  360. self.visit_page( "clear_history" )
  361. self.check_history_for_string( 'Your history is empty' )
  362. self.home()
  363. def delete_history( self, id ):
  364. """Deletes one or more histories"""
  365. history_list = self.get_histories_as_data_list()
  366. self.assertTrue( history_list )
  367. num_deleted = len( id.split( ',' ) )
  368. self.home()
  369. self.visit_page( "history/list?operation=delete&id=%s" % ( id ) )
  370. check_str = 'Deleted %d %s' % ( num_deleted, iff( num_deleted != 1, "histories", "history" ) )
  371. self.check_page_for_string( check_str )
  372. self.home()
  373. def delete_current_history( self, strings_displayed=[] ):
  374. """Deletes the current history"""
  375. self.home()
  376. self.visit_page( "history/delete_current" )
  377. for check_str in strings_displayed:
  378. self.check_page_for_string( check_str )
  379. self.home()
  380. def get_histories_as_data_list( self ):
  381. """Returns the data elements of all histories"""
  382. tree = self.histories_as_xml_tree()
  383. data_list = [ elem for elem in tree.findall("data") ]
  384. return data_list
  385. def get_history_as_data_list( self, show_deleted=False ):
  386. """Returns the data elements of a history"""
  387. tree = self.history_as_xml_tree( show_deleted=show_deleted )
  388. data_list = [ elem for elem in tree.findall("data") ]
  389. return data_list
  390. def history_as_xml_tree( self, show_deleted=False ):
  391. """Returns a parsed xml object of a history"""
  392. self.home()
  393. self.visit_page( 'history?as_xml=True&show_deleted=%s' % show_deleted )
  394. xml = self.last_page()
  395. tree = ElementTree.fromstring(xml)
  396. return tree
  397. def histories_as_xml_tree( self ):
  398. """Returns a parsed xml object of all histories"""
  399. self.home()
  400. self.visit_page( 'history/list_as_xml' )
  401. xml = self.last_page()
  402. tree = ElementTree.fromstring(xml)
  403. return tree
  404. def history_options( self, user=False, active_datasets=False, activatable_datasets=False, histories_shared_by_others=False ):
  405. """Mimics user clicking on history options link"""
  406. self.home()
  407. self.visit_page( "root/history_options" )
  408. if user:
  409. self.check_page_for_string( 'Previously</a> stored histories' )
  410. if active_datasets:
  411. self.check_page_for_string( 'Create</a> a new empty history' )
  412. self.check_page_for_string( 'Construct workflow</a> from current history' )
  413. self.check_page_for_string( 'Copy</a> current history' )
  414. self.check_page_for_string( 'Share</a> current history' )
  415. self.check_page_for_string( 'Change default permissions</a> for current history' )
  416. if histories_shared_by_others:
  417. self.check_page_for_string( 'Histories</a> shared with you by others' )
  418. if activatable_datasets:
  419. self.check_page_for_string( 'Show deleted</a> datasets in current history' )
  420. self.check_page_for_string( 'Rename</a> current history' )
  421. self.check_page_for_string( 'Delete</a> current history' )
  422. self.home()
  423. def new_history( self, name=None ):
  424. """Creates a new, empty history"""
  425. self.home()
  426. if name:
  427. self.visit_url( "%s/history_new?name=%s" % ( self.url, name ) )
  428. else:
  429. self.visit_url( "%s/history_new" % self.url )
  430. self.check_history_for_string('Your history is empty')
  431. self.home()
  432. def rename_history( self, id, old_name, new_name ):
  433. """Rename an existing history"""
  434. self.home()
  435. self.visit_page( "history/rename?id=%s&name=%s" % ( id, new_name ) )
  436. check_str = 'History: %s renamed to: %s' % ( old_name, urllib.unquote( new_name ) )
  437. self.check_page_for_string( check_str )
  438. self.home()
  439. def set_history( self ):
  440. """Sets the history (stores the cookies for this run)"""
  441. if self.history_id:
  442. self.home()
  443. self.visit_page( "history?id=%s" % self.history_id )
  444. else:
  445. self.new_history()
  446. self.home()
  447. def share_current_history( self, email, strings_displayed=[], strings_displayed_after_submit=[],
  448. action='', action_strings_displayed=[], action_strings_displayed_after_submit=[] ):
  449. """Share the current history with different users"""
  450. self.visit_url( "%s/history/share" % self.url )
  451. for check_str in strings_displayed:
  452. self.check_page_for_string( check_str )
  453. tc.fv( 'share', 'email', email )
  454. tc.submit( 'share_button' )
  455. for check_str in strings_displayed_after_submit:
  456. self.check_page_for_string( check_str )
  457. if action:
  458. # If we have an action, then we are sharing datasets with users that do not have access permissions on them
  459. for check_str in action_strings_displayed:
  460. self.check_page_for_string( check_str )
  461. tc.fv( 'share_restricted', 'action', action )
  462. tc.submit( "share_restricted_button" )
  463. for check_str in action_strings_displayed_after_submit:
  464. self.check_page_for_string( check_str )
  465. self.home()
  466. def share_histories_with_users( self, ids, emails, strings_displayed=[], strings_displayed_after_submit=[],
  467. action=None, action_strings_displayed=[] ):
  468. """Share one or more histories with one or more different users"""
  469. self.visit_url( "%s/history/list?id=%s&operation=Share" % ( self.url, ids ) )
  470. for check_str in strings_displayed:
  471. self.check_page_for_string( check_str )
  472. tc.fv( 'share', 'email', emails )
  473. tc.submit( 'share_button' )
  474. for check_str in strings_displayed_after_submit:
  475. self.check_page_for_string( check_str )
  476. if action:
  477. # If we have an action, then we are sharing datasets with users that do not have access permissions on them
  478. tc.fv( 'share_restricted', 'action', action )
  479. tc.submit( "share_restricted_button" )
  480. for check_str in action_strings_displayed:
  481. self.check_page_for_string( check_str )
  482. self.home()
  483. def unshare_history( self, history_id, user_id, strings_displayed=[] ):
  484. """Unshare a history that has been shared with another user"""
  485. self.visit_url( "%s/history/list?id=%s&operation=share+or+publish" % ( self.url, history_id ) )
  486. for check_str in strings_displayed:
  487. self.check_page_for_string( check_str )
  488. self.visit_url( "%s/history/sharing?unshare_user=%s&id=%s" % ( self.url, user_id, history_id ) )
  489. self.home()
  490. def switch_history( self, id='', name='' ):
  491. """Switches to a history in the current list of histories"""
  492. self.visit_url( "%s/history/list?operation=switch&id=%s" % ( self.url, id ) )
  493. if name:
  494. self.check_history_for_exact_string( name )
  495. self.home()
  496. def view_stored_active_histories( self, strings_displayed=[] ):
  497. self.home()
  498. self.visit_page( "history/list" )
  499. self.check_page_for_string( 'Saved Histories' )
  500. self.check_page_for_string( 'operation=Rename' )
  501. self.check_page_for_string( 'operation=Switch' )
  502. self.check_page_for_string( 'operation=Delete' )
  503. for check_str in strings_displayed:
  504. self.check_page_for_string( check_str )
  505. self.home()
  506. def view_stored_deleted_histories( self, strings_displayed=[] ):
  507. self.home()
  508. self.visit_page( "history/list?f-deleted=True" )
  509. self.check_page_for_string( 'Saved Histories' )
  510. self.check_page_for_string( 'operation=Undelete' )
  511. for check_str in strings_displayed:
  512. self.check_page_for_string( check_str )
  513. self.home()
  514. def view_shared_histories( self, strings_displayed=[] ):
  515. self.home()
  516. self.visit_page( "history/list_shared" )
  517. for check_str in strings_displayed:
  518. self.check_page_for_string( check_str )
  519. self.home()
  520. def copy_history( self, history_id, copy_choice, strings_displayed=[], strings_displayed_after_submit=[] ):
  521. self.home()
  522. self.visit_page( "history/copy?id=%s" % history_id )
  523. for check_str in strings_displayed:
  524. self.check_page_for_string( check_str )
  525. tc.fv( '1', 'copy_choice', copy_choice )
  526. tc.submit( 'copy_choice_button' )
  527. for check_str in strings_displayed_after_submit:
  528. self.check_page_for_string( check_str )
  529. self.home()
  530. def make_accessible_via_link( self, history_id, strings_displayed=[], strings_displayed_after_submit=[] ):
  531. self.home()
  532. self.visit_page( "history/list?operation=share+or+publish&id=%s" % history_id )
  533. for check_str in strings_displayed:
  534. self.check_page_for_string( check_str )
  535. # twill barfs on this form, possibly because it contains no fields, but not sure.
  536. # In any case, we have to mimic the form submission
  537. self.home()
  538. self.visit_page( 'history/sharing?id=%s&make_accessible_via_link=True' % history_id )
  539. for check_str in strings_displayed_after_submit:
  540. self.check_page_for_string( check_str )
  541. self.home()
  542. def disable_access_via_link( self, history_id, strings_displayed=[], strings_displayed_after_submit=[] ):
  543. self.home()
  544. self.visit_page( "history/list?operation=share+or+publish&id=%s" % history_id )
  545. for check_str in strings_displayed:
  546. self.check_page_for_string( check_str )
  547. # twill barfs on this form, possibly because it contains no fields, but not sure.
  548. # In any case, we have to mimic the form submission
  549. self.home()
  550. self.visit_page( 'history/sharing?id=%s&disable_link_access=True' % history_id )
  551. for check_str in strings_displayed_after_submit:
  552. self.check_page_for_string( check_str )
  553. self.home()
  554. def import_history_via_url( self, history_id, email, strings_displayed_after_submit=[] ):
  555. self.home()
  556. self.visit_page( "history/imp?&id=%s" % history_id )
  557. for check_str in strings_displayed_after_submit:
  558. self.check_page_for_string( check_str )
  559. self.home()
  560. # Functions associated with datasets (history items) and meta data
  561. def _get_job_stream_output( self, hda_id, stream, format ):
  562. self.visit_page( "datasets/%s/%s" % ( self.security.encode_id( hda_id ), stream ) )
  563. output = self.last_page()
  564. return self._format_stream( output, stream, format )
  565. def _format_stream( self, output, stream, format ):
  566. if format:
  567. msg = "---------------------- >> begin tool %s << -----------------------\n" % stream
  568. msg += output + "\n"
  569. msg += "----------------------- >> end tool %s << ------------------------\n" % stream
  570. else:
  571. msg = output
  572. return msg
  573. def get_job_stdout( self, hda_id, format=False ):
  574. return self._get_job_stream_output( hda_id, 'stdout', format )
  575. def get_job_stderr( self, hda_id, format=False ):
  576. return self._get_job_stream_output( hda_id, 'stderr', format )
  577. def _assert_dataset_state( self, elem, state ):
  578. if elem.get( 'state' ) != state:
  579. errmsg = "Expecting dataset state '%s', but state is '%s'. Dataset blurb: %s\n\n" % ( state, elem.get('state'), elem.text.strip() )
  580. errmsg += self.get_job_stderr( elem.get( 'id' ), format=True )
  581. raise AssertionError( errmsg )
  582. def check_metadata_for_string( self, patt, hid=None ):
  583. """Looks for 'patt' in the edit page when editing a dataset"""
  584. data_list = self.get_history_as_data_list()
  585. self.assertTrue( data_list )
  586. if hid is None: # take last hid
  587. elem = data_list[-1]
  588. hid = int( elem.get('hid') )
  589. self.assertTrue( hid )
  590. self.visit_page( "dataset/edit?hid=%s" % hid )
  591. for subpatt in patt.split():
  592. tc.find(subpatt)
  593. def delete_history_item( self, hda_id, strings_displayed=[] ):
  594. """Deletes an item from a history"""
  595. try:
  596. hda_id = int( hda_id )
  597. except:
  598. raise AssertionError( "Invalid hda_id '%s' - must be int" % hda_id )
  599. self.visit_url( "%s/datasets/%s/delete?show_deleted_on_refresh=False" % ( self.url, self.security.encode_id( hda_id ) ) )
  600. for check_str in strings_displayed:
  601. self.check_page_for_string( check_str )
  602. def undelete_history_item( self, hda_id, strings_displayed=[] ):
  603. """Un-deletes a deleted item in a history"""
  604. try:
  605. hda_id = int( hda_id )
  606. except:
  607. raise AssertionError( "Invalid hda_id '%s' - must be int" % hda_id )
  608. self.visit_url( "%s/datasets/%s/undelete" % ( self.url, self.security.encode_id( hda_id ) ) )
  609. for check_str in strings_displayed:
  610. self.check_page_for_string( check_str )
  611. def display_history_item( self, hda_id, strings_displayed=[] ):
  612. """Displays a history item - simulates eye icon click"""
  613. self.visit_url( '%s/datasets/%s/display/' % ( self.url, self.security.encode_id( hda_id ) ) )
  614. for check_str in strings_displayed:
  615. self.check_page_for_string( check_str )
  616. self.home()
  617. def view_history( self, history_id, strings_displayed=[] ):
  618. """Displays a history for viewing"""
  619. self.visit_url( '%s/history/view?id=%s' % ( self.url, self.security.encode_id( history_id ) ) )
  620. for check_str in strings_displayed:
  621. self.check_page_for_string( check_str )
  622. self.home()
  623. def edit_hda_attribute_info( self, hda_id, new_name='', new_info='', new_dbkey='', new_startcol='',
  624. strings_displayed=[], strings_not_displayed=[] ):
  625. """Edit history_dataset_association attribute information"""
  626. self.home()
  627. self.visit_url( "%s/datasets/%s/edit" % ( self.url, self.security.encode_id( hda_id ) ) )
  628. submit_required = False
  629. self.check_page_for_string( 'Edit Attributes' )
  630. if new_name:
  631. tc.fv( 'edit_attributes', 'name', new_name )
  632. submit_required = True
  633. if new_info:
  634. tc.fv( 'edit_attributes', 'info', new_info )
  635. submit_required = True
  636. if new_dbkey:
  637. tc.fv( 'edit_attributes', 'dbkey', new_dbkey )
  638. submit_required = True
  639. if new_startcol:
  640. tc.fv( 'edit_attributes', 'startCol', new_startcol )
  641. submit_required = True
  642. if submit_required:
  643. tc.submit( 'save' )
  644. self.check_page_for_string( 'Attributes updated' )
  645. for check_str in strings_displayed:
  646. self.check_page_for_string( check_str )
  647. for check_str in strings_not_displayed:
  648. try:
  649. self.check_page_for_string( check_str )
  650. raise AssertionError( "String (%s) incorrectly displayed on Edit Attributes page." % check_str )
  651. except:
  652. pass
  653. self.home()
  654. def check_hda_attribute_info( self, hda_id, strings_displayed=[] ):
  655. """Edit history_dataset_association attribute information"""
  656. for check_str in strings_displayed:
  657. self.check_page_for_string( check_str )
  658. def auto_detect_metadata( self, hda_id ):
  659. """Auto-detect history_dataset_association metadata"""
  660. self.home()
  661. self.visit_url( "%s/datasets/%s/edit" % ( self.url, self.security.encode_id( hda_id ) ) )
  662. self.check_page_for_string( 'This will inspect the dataset and attempt' )
  663. tc.fv( 'auto_detect', 'detect', 'Auto-detect' )
  664. tc.submit( 'detect' )
  665. try:
  666. self.check_page_for_string( 'Attributes have been queued to be updated' )
  667. self.wait()
  668. except AssertionError:
  669. self.check_page_for_string( 'Attributes updated' )
  670. #self.check_page_for_string( 'Attributes updated' )
  671. self.home()
  672. def convert_format( self, hda_id, target_type ):
  673. """Convert format of history_dataset_association"""
  674. self.home()
  675. self.visit_url( "%s/datasets/%s/edit" % ( self.url, self.security.encode_id( hda_id ) ) )
  676. self.check_page_for_string( 'This will inspect the dataset and attempt' )
  677. tc.fv( 'convert_data', 'target_type', target_type )
  678. tc.submit( 'convert_data' )
  679. self.check_page_for_string( 'The file conversion of Convert BED to GFF on data' )
  680. self.wait() # wait for the format convert tool to finish before returning
  681. self.home()
  682. def change_datatype( self, hda_id, datatype ):
  683. """Change format of history_dataset_association"""
  684. self.home()
  685. self.visit_url( "%s/datasets/%s/edit" % ( self.url, self.security.encode_id( hda_id ) ) )
  686. self.check_page_for_string( 'This will change the datatype of the existing dataset but' )
  687. tc.fv( 'change_datatype', 'datatype', datatype )
  688. tc.submit( 'change' )
  689. self.check_page_for_string( 'Changed the type of dataset' )
  690. self.home()
  691. def copy_history_item( self, source_dataset_id=None, target_history_id=None, all_target_history_ids=[],
  692. deleted_history_ids=[] ):
  693. """
  694. Copy 1 history_dataset_association to 1 history (Limited by twill since it doesn't support multiple
  695. field names, such as checkboxes
  696. """
  697. self.home()
  698. self.visit_url( "%s/dataset/copy_datasets?source_dataset_ids=%s" % ( self.url, source_dataset_id ) )
  699. self.check_page_for_string( 'Source History:' )
  700. # Make sure all of users active histories are displayed
  701. for id in all_target_history_ids:
  702. self.check_page_for_string( id )
  703. # Make sure only active histories are displayed
  704. for id in deleted_history_ids:
  705. try:
  706. self.check_page_for_string( id )
  707. raise AssertionError( "deleted history id %d displayed in list of target histories" % id )
  708. except:
  709. pass
  710. tc.fv( '1', 'target_history_id', target_history_id )
  711. tc.submit( 'do_copy' )
  712. check_str = '1 dataset copied to 1 history'
  713. self.check_page_for_string( check_str )
  714. self.home()
  715. def get_hids_in_history( self ):
  716. """Returns the list of hid values for items in a history"""
  717. data_list = self.get_history_as_data_list()
  718. hids = []
  719. for elem in data_list:
  720. hid = elem.get('hid')
  721. hids.append(hid)
  722. return hids
  723. def get_hids_in_histories( self ):
  724. """Returns the list of hids values for items in all histories"""
  725. data_list = self.get_histories_as_data_list()
  726. hids = []
  727. for elem in data_list:
  728. hid = elem.get('hid')
  729. hids.append(hid)
  730. return hids
  731. def makeTfname(self, fname=None):
  732. """safe temp name - preserve the file extension for tools that interpret it"""
  733. suffix = os.path.split(fname)[-1] # ignore full path
  734. fd, temp_prefix = tempfile.mkstemp(prefix='tmp', suffix=suffix)
  735. return temp_prefix
  736. def verify_dataset_correctness( self, filename, hid=None, wait=True, maxseconds=120, attributes=None, shed_tool_id=None ):
  737. """Verifies that the attributes and contents of a history item meet expectations"""
  738. if wait:
  739. self.wait( maxseconds=maxseconds ) # wait for job to finish
  740. data_list = self.get_history_as_data_list()
  741. self.assertTrue( data_list )
  742. if hid is None: # take last hid
  743. elem = data_list[-1]
  744. hid = str( elem.get('hid') )
  745. else:
  746. hid = str( hid )
  747. elems = [ elem for elem in data_list if elem.get('hid') == hid ]
  748. self.assertTrue( len(elems) == 1 )
  749. elem = elems[0]
  750. self.assertTrue( hid )
  751. self._assert_dataset_state( elem, 'ok' )
  752. if filename is not None and self.is_zipped( filename ):
  753. errmsg = 'History item %s is a zip archive which includes invalid files:\n' % hid
  754. zip_file = zipfile.ZipFile( filename, "r" )
  755. name = zip_file.namelist()[0]
  756. test_ext = name.split( "." )[1].strip().lower()
  757. if not ( test_ext == 'scf' or test_ext == 'ab1' or test_ext == 'txt' ):
  758. raise AssertionError( errmsg )
  759. for name in zip_file.namelist():
  760. ext = name.split( "." )[1].strip().lower()
  761. if ext != test_ext:
  762. raise AssertionError( errmsg )
  763. else:
  764. # See not in controllers/root.py about encoded_id.
  765. hda_id = self.security.encode_id( elem.get( 'id' ) )
  766. self.verify_hid( filename, hid=hid, hda_id=hda_id, attributes=attributes, shed_tool_id=shed_tool_id)
  767. def verify_hid( self, filename, hda_id, attributes, shed_tool_id, hid="", dataset_fetcher=None):
  768. dataset_fetcher = dataset_fetcher or self.__default_dataset_fetcher()
  769. data = dataset_fetcher( hda_id )
  770. if attributes is not None and attributes.get( "assert_list", None ) is not None:
  771. try:
  772. verify_assertions(data, attributes["assert_list"])
  773. except AssertionError, err:
  774. errmsg = 'History item %s different than expected\n' % (hid)
  775. errmsg += str( err )
  776. raise AssertionError( errmsg )
  777. if filename is not None:
  778. local_name = self.get_filename( filename, shed_tool_id=shed_tool_id )
  779. temp_name = self.makeTfname(fname=filename)
  780. file( temp_name, 'wb' ).write( data )
  781. # if the server's env has GALAXY_TEST_SAVE, save the output file to that dir
  782. if self.keepOutdir:
  783. ofn = os.path.join( self.keepOutdir, os.path.basename( local_name ) )
  784. log.debug( 'keepoutdir: %s, ofn: %s', self.keepOutdir, ofn )
  785. try:
  786. shutil.copy( temp_name, ofn )
  787. except Exception, exc:
  788. error_log_msg = ( 'TwillTestCase could not save output file %s to %s: ' % ( temp_name, ofn ) )
  789. error_log_msg += str( exc )
  790. log.error( error_log_msg, exc_info=True )
  791. else:
  792. log.debug('## GALAXY_TEST_SAVE=%s. saved %s' % ( self.keepOutdir, ofn ) )
  793. try:
  794. if attributes is None:
  795. attributes = {}
  796. compare = attributes.get( 'compare', 'diff' )
  797. if attributes.get( 'ftype', None ) == 'bam':
  798. local_fh, temp_name = self._bam_to_sam( local_name, temp_name )
  799. local_name = local_fh.name
  800. extra_files = attributes.get( 'extra_files', None )
  801. if compare == 'diff':
  802. self.files_diff( local_name, temp_name, attributes=attributes )
  803. elif compare == 're_match':
  804. self.files_re_match( local_name, temp_name, attributes=attributes )
  805. elif compare == 're_match_multiline':
  806. self.files_re_match_multiline( local_name, temp_name, attributes=attributes )
  807. elif compare == 'sim_size':
  808. delta = attributes.get('delta', '100')
  809. s1 = len(data)
  810. s2 = os.path.getsize(local_name)
  811. if abs(s1 - s2) > int(delta):
  812. raise Exception( 'Files %s=%db but %s=%db - compare (delta=%s) failed' % (temp_name, s1, local_name, s2, delta) )
  813. elif compare == "contains":
  814. self.files_contains( local_name, temp_name, attributes=attributes )
  815. else:
  816. raise Exception( 'Unimplemented Compare type: %s' % compare )
  817. if extra_files:
  818. self.verify_extra_files_content( extra_files, hda_id, shed_tool_id=shed_tool_id, dataset_fetcher=dataset_fetcher )
  819. except AssertionError, err:
  820. errmsg = 'History item %s different than expected, difference (using %s):\n' % ( hid, compare )
  821. errmsg += "( %s v. %s )\n" % ( local_name, temp_name )
  822. errmsg += str( err )
  823. raise AssertionError( errmsg )
  824. finally:
  825. if 'GALAXY_TEST_NO_CLEANUP' not in os.environ:
  826. os.remove( temp_name )
  827. def __default_dataset_fetcher( self ):
  828. def fetcher( hda_id, filename=None ):
  829. if filename is None:
  830. page_url = "display?encoded_id=%s" % hda_id
  831. self.home() # I assume this is not needed.
  832. else:
  833. page_url = "datasets/%s/display/%s" % ( hda_id, filename )
  834. self.visit_page( page_url )
  835. data = self.last_page()
  836. return data
  837. return fetcher
  838. def _bam_to_sam( self, local_name, temp_name ):
  839. temp_local = tempfile.NamedTemporaryFile( suffix='.sam', prefix='local_bam_converted_to_sam_' )
  840. fd, temp_temp = tempfile.mkstemp( suffix='.sam', prefix='history_bam_converted_to_sam_' )
  841. os.close( fd )
  842. p = subprocess.Popen( args='samtools view -h -o "%s" "%s"' % ( temp_local.name, local_name ), shell=True )
  843. assert not p.wait(), 'Converting local (test-data) bam to sam failed'
  844. p = subprocess.Popen( args='samtools view -h -o "%s" "%s"' % ( temp_temp, temp_name ), shell=True )
  845. assert not p.wait(), 'Converting history bam to sam failed'
  846. os.remove( temp_name )
  847. return temp_local, temp_temp
  848. def verify_extra_files_content( self, extra_files, hda_id, dataset_fetcher, shed_tool_id=None ):
  849. files_list = []
  850. for extra_type, extra_value, extra_name, extra_attributes in extra_files:
  851. if extra_type == 'file':
  852. files_list.append( ( extra_name, extra_value, extra_attributes ) )
  853. elif extra_type == 'directory':
  854. for filename in os.listdir( self.get_filename( extra_value, shed_tool_id=shed_tool_id ) ):
  855. files_list.append( ( filename, os.path.join( extra_value, filename ), extra_attributes ) )
  856. else:
  857. raise ValueError( 'unknown extra_files type: %s' % extra_type )
  858. for filename, filepath, attributes in files_list:
  859. self.verify_composite_datatype_file_content( filepath, hda_id, base_name=filename, attributes=attributes, dataset_fetcher=dataset_fetcher, shed_tool_id=shed_tool_id )
  860. def verify_composite_datatype_file_content( self, file_name, hda_id, base_name=None, attributes=None, dataset_fetcher=None, shed_tool_id=None ):
  861. dataset_fetcher = dataset_fetcher or self.__default_dataset_fetcher()
  862. local_name = self.get_filename( file_name, shed_tool_id=shed_tool_id )
  863. if base_name is None:
  864. base_name = os.path.split(file_name)[-1]
  865. temp_name = self.makeTfname(fname=base_name)
  866. data = dataset_fetcher( hda_id, base_name )
  867. file( temp_name, 'wb' ).write( data )
  868. if self.keepOutdir > '':
  869. ofn = os.path.join(self.keepOutdir, base_name)
  870. shutil.copy(temp_name, ofn)
  871. log.debug('## GALAXY_TEST_SAVE=%s. saved %s' % (self.keepOutdir, ofn))
  872. try:
  873. if attributes is None:
  874. attributes = {}
  875. compare = attributes.get( 'compare', 'diff' )
  876. if compare == 'diff':
  877. self.files_diff( local_name, temp_name, attributes=attributes )
  878. elif compare == 're_match':
  879. self.files_re_match( local_name, temp_name, attributes=attributes )
  880. elif compare == 're_match_multiline':
  881. self.files_re_match_multiline( local_name, temp_name, attributes=attributes )
  882. elif compare == 'sim_size':
  883. delta = attributes.get('delta', '100')
  884. s1 = len(data)
  885. s2 = os.path.getsize(local_name)
  886. if abs(s1 - s2) > int(delta):
  887. raise Exception( 'Files %s=%db but %s=%db - compare (delta=%s) failed' % (temp_name, s1, local_name, s2, delta) )
  888. else:
  889. raise Exception( 'Unimplemented Compare type: %s' % compare )
  890. except AssertionError, err:
  891. errmsg = 'Composite file (%s) of History item %s different than expected, difference (using %s):\n' % ( base_name, hda_id, compare )
  892. errmsg += str( err )
  893. raise AssertionError( errmsg )
  894. finally:
  895. if 'GALAXY_TEST_NO_CLEANUP' not in os.environ:
  896. os.remove( temp_name )
  897. def is_zipped( self, filename ):
  898. if not zipfile.is_zipfile( filename ):
  899. return False
  900. return True
  901. def is_binary( self, filename ):
  902. temp = open( filename, "U" ) # why is this not filename? Where did temp_name come from
  903. lineno = 0
  904. for line in temp:
  905. lineno += 1
  906. line = line.strip()
  907. if line:
  908. for char in line:
  909. if ord( char ) > 128:
  910. return True
  911. if lineno > 10:
  912. break
  913. return False
  914. def verify_genome_build( self, dbkey='hg17' ):
  915. """Verifies that the last used genome_build at history id 'hid' is as expected"""
  916. data_list = self.get_history_as_data_list()
  917. self.assertTrue( data_list )
  918. elems = [ elem for elem in data_list ]
  919. elem = elems[-1]
  920. genome_build = elem.get('dbkey')
  921. self.assertTrue( genome_build == dbkey )
  922. # Functions associated with user accounts
  923. def create( self, cntrller='user', email='test@bx.psu.edu', password='testuser', username='admin-user', redirect='' ):
  924. # HACK: don't use panels because late_javascripts() messes up the twill browser and it
  925. # can't find form fields (and hence user can't be logged in).
  926. self.visit_url( "%s/user/create?cntrller=%s&use_panels=False" % ( self.url, cntrller ) )
  927. tc.fv( 'registration', 'email', email )
  928. tc.fv( 'registration', 'redirect', redirect )
  929. tc.fv( 'registration', 'password', password )
  930. tc.fv( 'registration', 'confirm', password )
  931. tc.fv( 'registration', 'username', username )
  932. tc.submit( 'create_user_button' )
  933. previously_created = False
  934. username_taken = False
  935. invalid_username = False
  936. try:
  937. self.check_page_for_string( "Created new user account" )
  938. except:
  939. try:
  940. # May have created the account in a previous test run...
  941. self.check_page_for_string( "User with that email already exists" )
  942. previously_created = True
  943. except:
  944. try:
  945. self.check_page_for_string( 'Public name is taken; please choose another' )
  946. username_taken = True
  947. except:
  948. try:
  949. # Note that we're only checking if the usr name is >< 4 chars here...
  950. self.check_page_for_string( 'Public name must be at least 4 characters in length' )