/gdata/test_config.py
Python | 403 lines | 362 code | 8 blank | 33 comment | 31 complexity | 7fbcc3e60209268332732a8255239689 MD5 | raw file
1#!/usr/bin/env python 2 3# Copyright (C) 2009 Google Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17 18import sys 19import unittest 20import getpass 21import inspect 22import atom.mock_http_core 23import gdata.gauth 24 25 26"""Loads configuration for tests which connect to Google servers. 27 28Settings used in tests are stored in a ConfigCollection instance in this 29module called options. If your test needs to get a test related setting, 30use 31 32import gdata.test_config 33option_value = gdata.test_config.options.get_value('x') 34 35The above will check the command line for an '--x' argument, and if not 36found will either use the default value for 'x' or prompt the user to enter 37one. 38 39Your test can override the value specified by the user by performing: 40 41gdata.test_config.options.set_value('x', 'y') 42 43If your test uses a new option which you would like to allow the user to 44specify on the command line or via a prompt, you can use the register_option 45method as follows: 46 47gdata.test_config.options.register( 48 'option_name', 'Prompt shown to the user', secret=False #As for password. 49 'This is the description of the option, shown when help is requested.', 50 'default value, provide only if you do not want the user to be prompted') 51""" 52 53 54class Option(object): 55 56 def __init__(self, name, prompt, secret=False, description=None, default=None): 57 self.name = name 58 self.prompt = prompt 59 self.secret = secret 60 self.description = description 61 self.default = default 62 63 def get(self): 64 value = self.default 65 # Check for a command line parameter. 66 for i in xrange(len(sys.argv)): 67 if sys.argv[i].startswith('--%s=' % self.name): 68 value = sys.argv[i].split('=')[1] 69 elif sys.argv[i] == '--%s' % self.name: 70 value = sys.argv[i + 1] 71 # If the param was not on the command line, ask the user to input the 72 # value. 73 # In order for this to prompt the user, the default value for the option 74 # must be None. 75 if value is None: 76 prompt = '%s: ' % self.prompt 77 if self.secret: 78 value = getpass.getpass(prompt) 79 else: 80 print 'You can specify this on the command line using --%s' % self.name 81 value = raw_input(prompt) 82 return value 83 84 85class ConfigCollection(object): 86 87 def __init__(self, options=None): 88 self.options = options or {} 89 self.values = {} 90 91 def register_option(self, option): 92 self.options[option.name] = option 93 94 def register(self, *args, **kwargs): 95 self.register_option(Option(*args, **kwargs)) 96 97 def get_value(self, option_name): 98 if option_name in self.values: 99 return self.values[option_name] 100 value = self.options[option_name].get() 101 if value is not None: 102 self.values[option_name] = value 103 return value 104 105 def set_value(self, option_name, value): 106 self.values[option_name] = value 107 108 def render_usage(self): 109 message_parts = [] 110 for opt_name, option in self.options.iteritems(): 111 message_parts.append('--%s: %s' % (opt_name, option.description)) 112 return '\n'.join(message_parts) 113 114 115options = ConfigCollection() 116 117 118# Register the default options. 119options.register( 120 'username', 121 'Please enter the email address of your test account', 122 description=('The email address you want to sign in with. ' 123 'Make sure this is a test account as these tests may edit' 124 ' or delete data.')) 125options.register( 126 'password', 127 'Please enter the password for your test account', 128 secret=True, description='The test account password.') 129options.register( 130 'clearcache', 131 'Delete cached data? (enter true or false)', 132 description=('If set to true, any temporary files which cache test' 133 ' requests and responses will be deleted.'), 134 default='true') 135options.register( 136 'savecache', 137 'Save requests and responses in a temporary file? (enter true or false)', 138 description=('If set to true, requests to the server and responses will' 139 ' be saved in temporary files.'), 140 default='false') 141options.register( 142 'runlive', 143 'Run the live tests which contact the server? (enter true or false)', 144 description=('If set to true, the tests will make real HTTP requests to' 145 ' the servers. This slows down test execution and may' 146 ' modify the users data, be sure to use a test account.'), 147 default='true') 148options.register( 149 'ssl', 150 'Run the live tests over SSL (enter true or false)', 151 description='If set to true, all tests will be performed over HTTPS (SSL)', 152 default='false') 153options.register( 154 'appsusername', 155 'Please enter the email address of your test Apps domain account', 156 description=('The email address you want to sign in with. ' 157 'Make sure this is a test account on your Apps domain as ' 158 'these tests may edit or delete data.')) 159options.register( 160 'appspassword', 161 'Please enter the password for your test Apps domain account', 162 secret=True, description='The test Apps account password.') 163 164# Other options which may be used if needed. 165BLOG_ID_OPTION = Option( 166 'blogid', 167 'Please enter the ID of your test blog', 168 description=('The blog ID for the blog which should have test posts added' 169 ' to it. Example 7682659670455539811')) 170TEST_IMAGE_LOCATION_OPTION = Option( 171 'imgpath', 172 'Please enter the full path to a test image to upload', 173 description=('This test image will be uploaded to a service which' 174 ' accepts a media file, it must be a jpeg.')) 175SPREADSHEET_ID_OPTION = Option( 176 'spreadsheetid', 177 'Please enter the ID of a spreadsheet to use in these tests', 178 description=('The spreadsheet ID for the spreadsheet which should be' 179 ' modified by theses tests.')) 180APPS_DOMAIN_OPTION = Option( 181 'appsdomain', 182 'Please enter your Google Apps domain', 183 description=('The domain the Google Apps is hosted on or leave blank' 184 ' if n/a')) 185SITES_NAME_OPTION = Option( 186 'sitename', 187 'Please enter name of your Google Site', 188 description='The webspace name of the Site found in its URL.') 189PROJECT_NAME_OPTION = Option( 190 'project_name', 191 'Please enter the name of your project hosting project', 192 description=('The name of the project which should have test issues added' 193 ' to it. Example gdata-python-client')) 194ISSUE_ASSIGNEE_OPTION = Option( 195 'issue_assignee', 196 'Enter the email address of the target owner of the updated issue.', 197 description=('The email address of the user a created issue\'s owner will ' 198 ' become. Example testuser2@gmail.com')) 199GA_TABLE_ID = Option( 200 'table_id', 201 'Enter the Table ID of the Google Analytics profile to test', 202 description=('The Table ID of the Google Analytics profile to test.' 203 ' Example ga:1174')) 204 205# Functions to inject a cachable HTTP client into a service client. 206def configure_client(client, case_name, service_name, use_apps_auth=False): 207 """Sets up a mock client which will reuse a saved session. 208 209 Should be called during setUp of each unit test. 210 211 Handles authentication to allow the GDClient to make requests which 212 require an auth header. 213 214 Args: 215 client: a gdata.GDClient whose http_client member should be replaced 216 with a atom.mock_http_core.MockHttpClient so that repeated 217 executions can used cached responses instead of contacting 218 the server. 219 case_name: str The name of the test case class. Examples: 'BloggerTest', 220 'ContactsTest'. Used to save a session 221 for the ClientLogin auth token request, so the case_name 222 should be reused if and only if the same username, password, 223 and service are being used. 224 service_name: str The service name as used for ClientLogin to identify 225 the Google Data API being accessed. Example: 'blogger', 226 'wise', etc. 227 use_apps_auth: bool (optional) If set to True, use appsusername and 228 appspassword command-line args instead of username and 229 password respectively. 230 """ 231 # Use a mock HTTP client which will record and replay the HTTP traffic 232 # from these tests. 233 client.http_client = atom.mock_http_core.MockHttpClient() 234 client.http_client.cache_case_name = case_name 235 # Getting the auth token only needs to be done once in the course of test 236 # runs. 237 auth_token_key = '%s_auth_token' % service_name 238 if (auth_token_key not in options.values 239 and options.get_value('runlive') == 'true'): 240 client.http_client.cache_test_name = 'client_login' 241 cache_name = client.http_client.get_cache_file_name() 242 if options.get_value('clearcache') == 'true': 243 client.http_client.delete_session(cache_name) 244 client.http_client.use_cached_session(cache_name) 245 if not use_apps_auth: 246 username = options.get_value('username') 247 password = options.get_value('password') 248 else: 249 username = options.get_value('appsusername') 250 password = options.get_value('appspassword') 251 auth_token = client.request_client_login_token(username, password, 252 case_name, service=service_name) 253 options.values[auth_token_key] = gdata.gauth.token_to_blob(auth_token) 254 client.http_client.close_session() 255 # Allow a config auth_token of False to prevent the client's auth header 256 # from being modified. 257 if auth_token_key in options.values: 258 client.auth_token = gdata.gauth.token_from_blob( 259 options.values[auth_token_key]) 260 261 262def configure_cache(client, test_name): 263 """Loads or begins a cached session to record HTTP traffic. 264 265 Should be called at the beginning of each test method. 266 267 Args: 268 client: a gdata.GDClient whose http_client member has been replaced 269 with a atom.mock_http_core.MockHttpClient so that repeated 270 executions can used cached responses instead of contacting 271 the server. 272 test_name: str The name of this test method. Examples: 273 'TestClass.test_x_works', 'TestClass.test_crud_operations'. 274 This is used to name the recording of the HTTP requests and 275 responses, so it should be unique to each test method in the 276 test case. 277 """ 278 # Auth token is obtained in configure_client which is called as part of 279 # setUp. 280 client.http_client.cache_test_name = test_name 281 cache_name = client.http_client.get_cache_file_name() 282 if options.get_value('clearcache') == 'true': 283 client.http_client.delete_session(cache_name) 284 client.http_client.use_cached_session(cache_name) 285 286 287def close_client(client): 288 """Saves the recoded responses to a temp file if the config file allows. 289 290 This should be called in the unit test's tearDown method. 291 292 Checks to see if the 'savecache' option is set to 'true', to make sure we 293 only save sessions to repeat if the user desires. 294 """ 295 if client and options.get_value('savecache') == 'true': 296 # If this was a live request, save the recording. 297 client.http_client.close_session() 298 299 300def configure_service(service, case_name, service_name): 301 """Sets up a mock GDataService v1 client to reuse recorded sessions. 302 303 Should be called during setUp of each unit test. This is a duplicate of 304 configure_client, modified to handle old v1 service classes. 305 """ 306 service.http_client.v2_http_client = atom.mock_http_core.MockHttpClient() 307 service.http_client.v2_http_client.cache_case_name = case_name 308 # Getting the auth token only needs to be done once in the course of test 309 # runs. 310 auth_token_key = 'service_%s_auth_token' % service_name 311 if (auth_token_key not in options.values 312 and options.get_value('runlive') == 'true'): 313 service.http_client.v2_http_client.cache_test_name = 'client_login' 314 cache_name = service.http_client.v2_http_client.get_cache_file_name() 315 if options.get_value('clearcache') == 'true': 316 service.http_client.v2_http_client.delete_session(cache_name) 317 service.http_client.v2_http_client.use_cached_session(cache_name) 318 service.ClientLogin(options.get_value('username'), 319 options.get_value('password'), 320 service=service_name, source=case_name) 321 options.values[auth_token_key] = service.GetClientLoginToken() 322 service.http_client.v2_http_client.close_session() 323 if auth_token_key in options.values: 324 service.SetClientLoginToken(options.values[auth_token_key]) 325 326 327def configure_service_cache(service, test_name): 328 """Loads or starts a session recording for a v1 Service object. 329 330 Duplicates the behavior of configure_cache, but the target for this 331 function is a v1 Service object instead of a v2 Client. 332 """ 333 service.http_client.v2_http_client.cache_test_name = test_name 334 cache_name = service.http_client.v2_http_client.get_cache_file_name() 335 if options.get_value('clearcache') == 'true': 336 service.http_client.v2_http_client.delete_session(cache_name) 337 service.http_client.v2_http_client.use_cached_session(cache_name) 338 339 340def close_service(service): 341 if service and options.get_value('savecache') == 'true': 342 # If this was a live request, save the recording. 343 service.http_client.v2_http_client.close_session() 344 345 346def build_suite(classes): 347 """Creates a TestSuite for all unit test classes in the list. 348 349 Assumes that each of the classes in the list has unit test methods which 350 begin with 'test'. Calls unittest.makeSuite. 351 352 Returns: 353 A new unittest.TestSuite containing a test suite for all classes. 354 """ 355 suites = [unittest.makeSuite(a_class, 'test') for a_class in classes] 356 return unittest.TestSuite(suites) 357 358 359def check_data_classes(test, classes): 360 import inspect 361 for data_class in classes: 362 test.assert_(data_class.__doc__ is not None, 363 'The class %s should have a docstring' % data_class) 364 if hasattr(data_class, '_qname'): 365 qname_versions = None 366 if isinstance(data_class._qname, tuple): 367 qname_versions = data_class._qname 368 else: 369 qname_versions = (data_class._qname,) 370 for versioned_qname in qname_versions: 371 test.assert_(isinstance(versioned_qname, str), 372 'The class %s has a non-string _qname' % data_class) 373 test.assert_(not versioned_qname.endswith('}'), 374 'The _qname for class %s is only a namespace' % ( 375 data_class)) 376 377 for attribute_name, value in data_class.__dict__.iteritems(): 378 # Ignore all elements that start with _ (private members) 379 if not attribute_name.startswith('_'): 380 try: 381 if not (isinstance(value, str) or inspect.isfunction(value) 382 or (isinstance(value, list) 383 and issubclass(value[0], atom.core.XmlElement)) 384 or type(value) == property # Allow properties. 385 or inspect.ismethod(value) # Allow methods. 386 or issubclass(value, atom.core.XmlElement)): 387 test.fail( 388 'XmlElement member should have an attribute, XML class,' 389 ' or list of XML classes as attributes.') 390 391 except TypeError: 392 test.fail('Element %s in %s was of type %s' % ( 393 attribute_name, data_class._qname, type(value))) 394 395 396def check_clients_with_auth(test, classes): 397 for client_class in classes: 398 test.assert_(hasattr(client_class, 'api_version')) 399 test.assert_(isinstance(client_class.auth_service, (str, unicode, int))) 400 test.assert_(hasattr(client_class, 'auth_service')) 401 test.assert_(isinstance(client_class.auth_service, (str, unicode))) 402 test.assert_(hasattr(client_class, 'auth_scopes')) 403 test.assert_(isinstance(client_class.auth_scopes, (list, tuple)))