PageRenderTime 130ms CodeModel.GetById 23ms app.highlight 92ms RepoModel.GetById 1ms app.codeStats 1ms

/test/base/twilltestcase.py

https://bitbucket.org/cistrome/cistrome-harvard/
Python | 2600 lines | 2584 code | 9 blank | 7 comment | 39 complexity | df943a8c086a1e8b21a50a6cb0a60990 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

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

Large files files are truncated, but you can click here to view the full file