| OLD | NEW |
| 1 # -*- coding: utf-8 -*- | 1 # -*- coding: utf-8 -*- |
| 2 # Copyright 2014 The Chromium Authors. All rights reserved. | 2 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """This file allows the bots to be easily configure and run the tests.""" | 6 """This file allows the bots to be easily configure and run the tests. |
| 7 | 7 |
| 8 Running this script requires passing --config-path with a path to a config file |
| 9 of the following structure: |
| 10 [credentials] |
| 11 pkey=full_path |
| 12 client_email=email_assigned_by_google_dev_console |
| 13 [drive] |
| 14 key=sheet_key_from_sheet_url |
| 15 [data_files] |
| 16 passwords_path=full_path_to_the_file_with_passwords |
| 17 """ |
| 18 from Sheet import Sheet |
| 19 from apiclient.discovery import build |
| 20 from datetime import datetime |
| 21 from gdata.gauth import OAuth2TokenFromCredentials |
| 22 from gdata.spreadsheet.service import SpreadsheetsService |
| 23 from oauth2client.client import SignedJwtAssertionCredentials |
| 24 import ConfigParser |
| 8 import argparse | 25 import argparse |
| 26 import httplib2 |
| 27 import oauth2client.tools |
| 9 import os | 28 import os |
| 10 import tempfile | 29 import tempfile |
| 11 | 30 |
| 12 from environment import Environment | 31 from environment import Environment |
| 13 import tests | 32 import tests |
| 14 | 33 |
| 34 _CREDENTIAL_SCOPES = "https://spreadsheets.google.com/feeds" |
| 35 |
| 36 # TODO(melandory): Function _authenticate belongs to separate module. |
| 37 def _authenticate(pkey, client_email): |
| 38 """ Authenticates user. |
| 39 |
| 40 Args: |
| 41 pkey: Full path to file with private key generated by Google |
| 42 Developer Console. |
| 43 client_email: Email address corresponding to private key and also |
| 44 generated by Google Developer Console. |
| 45 """ |
| 46 http, token = None, None |
| 47 with open(pkey) as pkey_file: |
| 48 private_key = pkey_file.read() |
| 49 credentials = SignedJwtAssertionCredentials( |
| 50 client_email, private_key, _CREDENTIAL_SCOPES) |
| 51 http = httplib2.Http() |
| 52 http = credentials.authorize(http) |
| 53 build('drive', 'v2', http=http) |
| 54 token = OAuth2TokenFromCredentials(credentials).access_token |
| 55 return http, token |
| 56 |
| 57 # TODO(melandory): Functionality of _spredsheeet_for_logging belongs |
| 58 # to websitetests, because this way we do not need to write results of run |
| 59 # in separate file and then read it here. |
| 60 def _spredsheeet_for_logging(sheet_key, access_token): |
| 61 """ Connects to document where result of test run will be logged. |
| 62 Args: |
| 63 sheet_key: Key of sheet in Trix. Can be found in sheet's URL. |
| 64 access_token: Access token of an account which should have edit rights. |
| 65 """ |
| 66 # Connect to trix |
| 67 service = SpreadsheetsService(additional_headers={ |
| 68 "Authorization": "Bearer " + access_token}) |
| 69 sheet = Sheet(service, sheet_key) |
| 70 return sheet |
| 71 |
| 72 def _try_run_individual_test(test_cmd, results_path): |
| 73 """ Runs individual test and logs results to trix. |
| 74 |
| 75 Args: |
| 76 test_cmd: String contains command which runs test. |
| 77 results_path: Full path to file where results of test run will be logged. |
| 78 """ |
| 79 failures = [] |
| 80 # The tests can be flaky. This is why we try to rerun up to 3 times. |
| 81 for x in xrange(3): |
| 82 # TODO(rchtara): Using "pkill" is just temporary until a better, |
| 83 # platform-independent solution is found. |
| 84 # Using any kind of kill and process name isn't best solution. |
| 85 # Mainly because it kills every running instace of Chrome on the machine, |
| 86 # which is unpleasant experience, when you're running tests locally. |
| 87 # In my opinion proper solution is to invoke chrome using subproceses and |
| 88 # then kill only this particular instance of it. |
| 89 os.system("pkill chrome") |
| 90 try: |
| 91 os.remove(results_path) |
| 92 except Exception: |
| 93 pass |
| 94 # TODO(rchtara): Using "timeout is just temporary until a better, |
| 95 # platform-independent solution is found. |
| 96 |
| 97 # The website test runs in two passes, each pass has an internal |
| 98 # timeout of 200s for waiting (see |remaining_time_to_wait| and |
| 99 # Wait() in websitetest.py). Accounting for some more time spent on |
| 100 # the non-waiting execution, 300 seconds should be the upper bound on |
| 101 # the runtime of one pass, thus 600 seconds for the whole test. |
| 102 # TODO(vabr): Use subprocess.call. |
| 103 os.system("timeout 600 %s" % test_cmd) |
| 104 if os.path.isfile(results_path): |
| 105 results = open(results_path, "r") |
| 106 count = 0 # Count the number of successful tests. |
| 107 for line in results: |
| 108 # TODO(melandory): We do not need to send all this data to sheet. |
| 109 failures.append(line) |
| 110 count += line.count("successful='True'") |
| 111 results.close() |
| 112 # There is only two tests running for every website: the prompt and |
| 113 # the normal test. If both of the tests were successful, the tests |
| 114 # would be stopped for the current website. |
| 115 if count == 2: |
| 116 return "pass", [] |
| 117 else: |
| 118 pass |
| 119 return "fail", failures |
| 120 |
| 121 |
| 15 def run_tests( | 122 def run_tests( |
| 16 save_path, chrome_path, chromedriver_path, | 123 chrome_path, chromedriver_path, |
| 17 passwords_path, profile_path, *args, **kwargs): | 124 profile_path, config_path, *args, **kwargs): |
| 18 """ Runs automated tests. | 125 """ Runs automated tests. |
| 19 | 126 |
| 20 Args: | 127 Args: |
| 21 save_path: File, where results of run will be logged. | 128 save_path: File, where results of run will be logged. |
| 22 chrome_path: Location of Chrome binary. | 129 chrome_path: Location of Chrome binary. |
| 23 chromedriver_path: Location of Chrome Driver. | 130 chromedriver_path: Location of Chrome Driver. |
| 24 passwords_path: Location of file with credentials. | 131 passwords_path: Location of file with credentials. |
| 25 profile_path: Location of profile path. | 132 profile_path: Location of profile path. |
| 26 *args: Variable length argument list. | 133 *args: Variable length argument list. |
| 27 **kwargs: Arbitrary keyword arguments. | 134 **kwargs: Arbitrary keyword arguments. |
| 28 """ | 135 """ |
| 29 environment = Environment('', '', '', None, False) | 136 environment = Environment('', '', '', None, False) |
| 30 tests.Tests(environment) | 137 tests.Tests(environment) |
| 31 | 138 config = ConfigParser.ConfigParser() |
| 32 xml = open(save_path, "w") | 139 config.read(config_path) |
| 33 xml.write("<xml>") | 140 date = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') |
| 34 try: | 141 try: |
| 142 _, access_token = _authenticate(config.get("credentials", "pkey"), |
| 143 config.get("credentials", "client_email")) |
| 144 sheet = _spredsheeet_for_logging(config.get("drive", "key"), access_token) |
| 35 results = tempfile.NamedTemporaryFile( | 145 results = tempfile.NamedTemporaryFile( |
| 36 dir=os.path.join(tempfile.gettempdir()), delete=False) | 146 dir=os.path.join(tempfile.gettempdir()), delete=False) |
| 37 results_path = results.name | 147 results_path = results.name |
| 38 results.close() | 148 results.close() |
| 39 | 149 |
| 40 full_path = os.path.realpath(__file__) | 150 full_path = os.path.realpath(__file__) |
| 41 tests_dir = os.path.dirname(full_path) | 151 tests_dir = os.path.dirname(full_path) |
| 42 tests_path = os.path.join(tests_dir, "tests.py") | 152 tests_path = os.path.join(tests_dir, "tests.py") |
| 43 | 153 |
| 44 for websitetest in environment.websitetests: | 154 for websitetest in environment.websitetests: |
| 45 # The tests can be flaky. This is why we try to rerun up to 3 times. | 155 test_cmd = ("python %s %s --chrome-path %s " |
| 46 for x in range(0, 3): | 156 "--chromedriver-path %s" |
| 47 # TODO(rchtara): Using "pkill" is just temporary until a better, | 157 "--passwords-path %s --profile-path %s " |
| 48 # platform-independent solution is found. | 158 "--save-path %s" % |
| 49 os.system("pkill chrome") | 159 (tests_path, websitetest.name, chrome_path, |
| 50 try: | 160 chromedriver_path, |
| 51 os.remove(results_path) | 161 config.get("data_files", "passwords_path"), |
| 52 except Exception: | 162 profile_path, results_path)) |
| 53 pass | 163 status, log = _try_run_individual_test( |
| 54 # TODO(rchtara): Using "timeout is just temporary until a better, | 164 websitetest, test_cmd, results_path) |
| 55 # platform-independent solution is found. | 165 try: |
| 56 | 166 sheet.InsertRow(sheet.row_count, |
| 57 # The website test runs in two passes, each pass has an internal | 167 [websitetest.name, status, date, " | ".join(log)]) |
| 58 # timeout of 200s for waiting (see |remaining_time_to_wait| and | 168 except Exception: |
| 59 # Wait() in websitetest.py). Accounting for some more time spent on | 169 pass # TODO(melandory): Sometimes writing to spreadsheet fails. We need |
| 60 # the non-waiting execution, 300 seconds should be the upper bound on | 170 # deal with it better that just ignoring. |
| 61 # the runtime of one pass, thus 600 seconds for the whole test. | |
| 62 os.system("timeout 600 python %s %s --chrome-path %s " | |
| 63 "--chromedriver-path %s --passwords-path %s --profile-path %s " | |
| 64 "--save-path %s" % | |
| 65 (tests_path, websitetest.name, chrome_path, | |
| 66 chromedriver_path, passwords_path, | |
| 67 profile_path, results_path)) | |
| 68 if os.path.isfile(results_path): | |
| 69 results = open(results_path, "r") | |
| 70 count = 0 # Count the number of successful tests. | |
| 71 for line in results: | |
| 72 xml.write(line) | |
| 73 count += line.count("successful='True'") | |
| 74 results.close() | |
| 75 # There is only two tests running for every website: the prompt and | |
| 76 # the normal test. If both of the tests were successful, the tests | |
| 77 # would be stopped for the current website. | |
| 78 if count == 2: | |
| 79 break | |
| 80 else: | |
| 81 xml.write("<result><test name='%s' type='prompt' successful='false'>" | |
| 82 "</test><test name='%s' type='normal' successful='false'></test>" | |
| 83 "</result>" % (websitetest.name, websitetest.name)) | |
| 84 finally: | 171 finally: |
| 85 try: | 172 try: |
| 86 os.remove(results_path) | 173 os.remove(results_path) |
| 87 except Exception: | 174 except Exception: |
| 88 pass | 175 pass |
| 89 | 176 |
| 90 xml.write("</xml>") | |
| 91 xml.close() | |
| 92 | 177 |
| 93 if __name__ == "__main__": | 178 if __name__ == "__main__": |
| 94 parser = argparse.ArgumentParser( | 179 parser = argparse.ArgumentParser( |
| 95 description="Password Manager automated tests runner help.") | 180 description="Password Manager automated tests runner help.") |
| 96 parser.add_argument( | 181 parser.add_argument( |
| 97 "--chrome-path", action="store", dest="chrome_path", | 182 "--chrome-path", action="store", dest="chrome_path", |
| 98 help="Set the chrome path (required).", required=True) | 183 help="Set the chrome path (required).", required=True) |
| 99 parser.add_argument( | 184 parser.add_argument( |
| 100 "--chromedriver-path", action="store", dest="chromedriver_path", | 185 "--chromedriver-path", action="store", dest="chromedriver_path", |
| 101 help="Set the chromedriver path (required).", required=True) | 186 help="Set the chromedriver path (required).", required=True) |
| 102 parser.add_argument( | 187 parser.add_argument( |
| 188 "--config-path", action="store", dest="config_path", |
| 189 help="File with configuration data: drive credentials, password path", |
| 190 required=True) |
| 191 parser.add_argument( |
| 103 "--profile-path", action="store", dest="profile_path", | 192 "--profile-path", action="store", dest="profile_path", |
| 104 help="Set the profile path (required). You just need to choose a " | 193 help="Set the profile path (required). You just need to choose a " |
| 105 "temporary empty folder. If the folder is not empty all its content " | 194 "temporary empty folder. If the folder is not empty all its content " |
| 106 "is going to be removed.", | 195 "is going to be removed.", |
| 107 required=True) | 196 required=True) |
| 108 parser.add_argument( | |
| 109 "--passwords-path", action="store", dest="passwords_path", | |
| 110 help="Set the usernames/passwords path (required).", required=True) | |
| 111 parser.add_argument("--save-path", action="store", dest="save_path", | |
| 112 help="Write the results in a file.", required=True) | |
| 113 args = vars(parser.parse_args()) | 197 args = vars(parser.parse_args()) |
| 114 run_tests(**args) | 198 run_tests(**args) |
| OLD | NEW |