Chromium Code Reviews| 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 | 8 Running this script requires passing --config-path with a path to a config file |
| 9 of the following structure: | 9 of the following structure: |
| 10 [credentials] | 10 [data_files] |
| 11 passwords_path=<path to a file with passwords> | |
| 12 [binaries] | |
| 13 chrome-path=<chrome binary path> | |
|
melandory
2015/02/06 08:37:47
Along with changes in run_tests you/I/someone shou
dvadym
2015/02/06 11:02:26
Thanks, I'll do.
| |
| 14 chromedriver-path=<chrome driver path> | |
| 15 [run_options] | |
| 16 write_to_sheet=[false|true] | |
| 17 tests_in_parrallel=<number of parrallel tests> | |
|
vabr (Chromium)
2015/02/05 17:49:58
parrallel -> parallel
dvadym
2015/02/06 11:02:27
Done.
| |
| 18 # |tests_to_runs| field is optional, if it is absent all tests will be run. | |
| 19 tests_to_run=<test names to run, comma delimited> | |
| 20 [output] | |
| 21 save-path=<file where to save result> | |
| 22 [sheet_info] | |
| 23 # This section is required only when write_to_sheet=true | |
| 11 pkey=full_path | 24 pkey=full_path |
| 12 client_email=email_assigned_by_google_dev_console | 25 client_email=email_assigned_by_google_dev_console |
| 13 [drive] | 26 sheet_key=sheet_key_from_sheet_url |
| 14 key=sheet_key_from_sheet_url | |
| 15 [data_files] | |
| 16 passwords_path=full_path_to_the_file_with_passwords | |
| 17 """ | 27 """ |
| 18 from Sheet import Sheet | |
| 19 from apiclient.discovery import build | |
| 20 from datetime import datetime | 28 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 | 29 import ConfigParser |
| 25 import argparse | 30 import sys |
| 26 import httplib2 | 31 import httplib2 |
| 27 import oauth2client.tools | |
| 28 import os | 32 import os |
| 29 import subprocess | 33 import subprocess |
| 30 import tempfile | 34 import tempfile |
| 35 import time | |
| 36 sheet_libraries_import_error = None | |
| 37 try: | |
| 38 from Sheet import Sheet | |
| 39 from apiclient.discovery import build | |
| 40 from gdata.gauth import OAuth2TokenFromCredentials | |
| 41 from gdata.spreadsheet.service import SpreadsheetsService | |
| 42 from oauth2client.client import SignedJwtAssertionCredentials | |
| 43 import oauth2client.tools | |
| 44 except ImportError as err: | |
| 45 sheet_libraries_import_error = err | |
| 46 | |
| 31 | 47 |
| 32 from environment import Environment | 48 from environment import Environment |
| 33 import tests | 49 import tests |
| 34 | 50 |
| 35 _CREDENTIAL_SCOPES = "https://spreadsheets.google.com/feeds" | 51 _CREDENTIAL_SCOPES = "https://spreadsheets.google.com/feeds" |
| 36 | 52 |
| 37 # TODO(melandory): Function _authenticate belongs to separate module. | 53 class SheetWriter: |
|
melandory
2015/02/06 08:37:47
Maybe put it to separate file?
dvadym
2015/02/06 11:02:27
It seems that run_tests.py is not so big, having m
vabr (Chromium)
2015/02/06 14:32:52
I don't support the thesis, that multiple files ar
dvadym
2015/02/06 15:00:16
Ok, added Todo
| |
| 38 def _authenticate(pkey, client_email): | 54 def __init__(self, config): |
| 39 """ Authenticates user. | 55 self.write_to_sheet = config.getboolean("run_options", "write_to_sheet") |
| 40 | 56 if not self.write_to_sheet: |
| 41 Args: | 57 return |
| 42 pkey: Full path to file with private key generated by Google | 58 if sheet_libraries_import_error != None: |
|
vabr (Chromium)
2015/02/05 17:49:57
nit: You could drop the != None
dvadym
2015/02/06 11:02:26
Done
| |
| 43 Developer Console. | 59 raise sheet_libraries_import_error |
| 44 client_email: Email address corresponding to private key and also | 60 self.pkey = config.get("sheet_info", "pkey") |
| 45 generated by Google Developer Console. | 61 self.client_mail=config.get("sheet_info", "client_email") |
| 46 """ | 62 self.sheet_key=config.get("sheet_info", "sheet_key") |
| 47 http, token = None, None | 63 _, self.access_token = self._authenticate() |
| 48 with open(pkey) as pkey_file: | 64 self.sheet = self._spredsheeet_for_logging() |
| 49 private_key = pkey_file.read() | 65 |
| 50 credentials = SignedJwtAssertionCredentials( | 66 # TODO(melandory): Function _authenticate belongs to separate module. |
| 51 client_email, private_key, _CREDENTIAL_SCOPES) | 67 def _authenticate(self): |
| 52 http = httplib2.Http() | 68 http, token = None, None |
| 53 http = credentials.authorize(http) | 69 with open(self.pkey) as pkey_file: |
| 54 build('drive', 'v2', http=http) | 70 private_key = pkey_file.read() |
| 55 token = OAuth2TokenFromCredentials(credentials).access_token | 71 credentials = SignedJwtAssertionCredentials( |
| 56 return http, token | 72 self.client_email, private_key, _CREDENTIAL_SCOPES) |
| 57 | 73 http = httplib2.Http() |
| 58 # TODO(melandory): Functionality of _spredsheeet_for_logging belongs | 74 http = credentials.authorize(http) |
| 59 # to websitetests, because this way we do not need to write results of run | 75 build('drive', 'v2', http=http) |
| 60 # in separate file and then read it here. | 76 token = OAuth2TokenFromCredentials(credentials).access_token |
| 61 def _spredsheeet_for_logging(sheet_key, access_token): | 77 return http, token |
| 62 """ Connects to document where result of test run will be logged. | 78 |
| 63 Args: | 79 # TODO(melandory): Functionality of _spredsheeet_for_logging belongs |
| 64 sheet_key: Key of sheet in Trix. Can be found in sheet's URL. | 80 # to websitetests, because this way we do not need to write results of run |
| 65 access_token: Access token of an account which should have edit rights. | 81 # in separate file and then read it here. |
| 66 """ | 82 def _spredsheeet_for_logging(self): |
| 67 # Connect to trix | 83 """ Connects to document where result of test run will be logged. """ |
| 68 service = SpreadsheetsService(additional_headers={ | 84 # Connect to trix |
| 69 "Authorization": "Bearer " + access_token}) | 85 service = SpreadsheetsService(additional_headers={ |
| 70 sheet = Sheet(service, sheet_key) | 86 "Authorization": "Bearer " + self.access_token}) |
| 71 return sheet | 87 sheet = Sheet(service, self.sheet_key) |
| 72 | 88 return sheet |
| 73 def _try_run_individual_test(test_cmd, results_path): | 89 |
| 74 """ Runs individual test and logs results to trix. | 90 def write_line_to_sheet(self, data): |
| 75 | 91 if not self.write_to_sheet: |
| 76 Args: | 92 return |
| 77 test_cmd: String contains command which runs test. | |
| 78 results_path: Full path to file where results of test run will be logged. | |
| 79 """ | |
| 80 failures = [] | |
| 81 # The tests can be flaky. This is why we try to rerun up to 3 times. | |
| 82 for x in xrange(3): | |
| 83 # TODO(rchtara): Using "pkill" is just temporary until a better, | |
| 84 # platform-independent solution is found. | |
| 85 # Using any kind of kill and process name isn't best solution. | |
| 86 # Mainly because it kills every running instace of Chrome on the machine, | |
| 87 # which is unpleasant experience, when you're running tests locally. | |
| 88 # In my opinion proper solution is to invoke chrome using subproceses and | |
| 89 # then kill only this particular instance of it. | |
| 90 os.system("pkill chrome") | |
| 91 try: | 93 try: |
| 92 os.remove(results_path) | 94 self.sheet.InsertRow(self.sheet.row_count, data) |
| 93 except Exception: | 95 except Exception: |
| 94 pass | 96 pass # TODO(melandory): Sometimes writing to spreadsheet fails. We need |
|
vabr (Chromium)
2015/02/05 17:49:57
grammar: need -> need to
dvadym
2015/02/06 11:02:26
Done.
| |
| 97 # deal with it better that just ignoring. | |
|
vabr (Chromium)
2015/02/05 17:49:57
that -> than
ignoring -> by ignoring it
dvadym
2015/02/06 11:02:27
Done.
| |
| 98 | |
| 99 class TestRunner: | |
| 100 def __init__(self, general_test_cmd, test_name): | |
| 101 """ Args: | |
| 102 general_test_cmd: String contains part of run command common for all tests. | |
| 103 test_name: Test name (facebook for example). | |
| 104 """ | |
| 105 print>>sys.stdout, "Test " + test_name + " starting" | |
|
vabr (Chromium)
2015/02/05 17:49:57
I'm not sure about the spacing. Did you run gpylin
vabr (Chromium)
2015/02/05 17:49:57
Also, do you need to specify sys.stdout?
dvadym
2015/02/06 11:02:26
Ah, yeah, thanks, at first I used sys.stderr, but
dvadym
2015/02/06 11:02:27
I didn't know about it, I run and fix.
| |
| 106 self.profile_path = tempfile.mkdtemp() | |
| 107 results = tempfile.NamedTemporaryFile( | |
| 108 dir=os.path.join(tempfile.gettempdir()), delete=False) | |
|
vabr (Chromium)
2015/02/05 17:49:58
What is the effect of calling path.join on a singl
dvadym
2015/02/06 11:02:26
It makes, sense, I removed tempfile.gettempdir())
| |
| 109 self.results_path = results.name | |
| 110 results.close() | |
| 111 self.test_cmd = general_test_cmd + ["--profile-path", self.profile_path, | |
| 112 "--save-path", self.results_path] | |
| 113 self.test_cmd[2] = self.test_name = test_name | |
|
vabr (Chromium)
2015/02/05 17:49:57
Are you relying on test_cmd to have a special form
dvadym
2015/02/06 11:02:26
Done. I've added comment in description of general
| |
| 95 # TODO(rchtara): Using "timeout is just temporary until a better, | 114 # TODO(rchtara): Using "timeout is just temporary until a better, |
| 96 # platform-independent solution is found. | 115 # platform-independent solution is found. |
| 97 | |
| 98 # The website test runs in two passes, each pass has an internal | 116 # The website test runs in two passes, each pass has an internal |
| 99 # timeout of 200s for waiting (see |remaining_time_to_wait| and | 117 # timeout of 200s for waiting (see |remaining_time_to_wait| and |
| 100 # Wait() in websitetest.py). Accounting for some more time spent on | 118 # Wait() in websitetest.py). Accounting for some more time spent on |
| 101 # the non-waiting execution, 300 seconds should be the upper bound on | 119 # the non-waiting execution, 300 seconds should be the upper bound on |
| 102 # the runtime of one pass, thus 600 seconds for the whole test. | 120 # the runtime of one pass, thus 600 seconds for the whole test. |
| 103 subprocess.call(["timeout", "600"] + test_cmd) | 121 self.test_cmd = ["timeout", "600"] + self.test_cmd |
| 104 if os.path.isfile(results_path): | 122 self.runner_process = None |
| 105 results = open(results_path, "r") | 123 # The tests can be flaky. This is why we try to rerun up to 3 times. |
| 124 self.max_test_runs_left = 3 | |
| 125 self.failures = [] | |
| 126 self._run_test() | |
| 127 | |
| 128 def get_test_result(self): | |
| 129 """ Return None if result is not ready yet.""" | |
| 130 test_running = (self.runner_process != None and | |
| 131 self.runner_process.poll() == None) | |
| 132 if test_running: return None | |
| 133 # Test is not running, now we have to check if we want to start it again. | |
| 134 if self._check_if_test_passed(): | |
| 135 print>>sys.stdout, "Test " + self.test_name + " passed" | |
|
melandory
2015/02/06 08:37:46
I would use logging instead of print, especially t
dvadym
2015/02/06 11:02:26
This is just matter of convenience for case when y
vabr (Chromium)
2015/02/06 14:32:52
In general, this kind of print statements should b
dvadym
2015/02/06 15:00:16
Ok, todo added
| |
| 136 return "pass", [] | |
| 137 if self.max_test_runs_left == 0: | |
| 138 print>>sys.stdout, "Test " + self.test_name + " failed" | |
| 139 return "fail", self.failures | |
| 140 self._run_test() | |
|
vabr (Chromium)
2015/02/05 17:49:58
no return?
dvadym
2015/02/06 11:02:27
In python no returns means return None, exactly as
vabr (Chromium)
2015/02/06 14:32:52
I don't like mixing the implicit return and the ex
dvadym
2015/02/06 15:00:16
Done.
| |
| 141 | |
| 142 def _check_if_test_passed(self): | |
| 143 if os.path.isfile(self.results_path): | |
| 144 results = open(self.results_path, "r") | |
| 106 count = 0 # Count the number of successful tests. | 145 count = 0 # Count the number of successful tests. |
| 107 for line in results: | 146 for line in results: |
| 108 # TODO(melandory): We do not need to send all this data to sheet. | 147 # TODO(melandory): We do not need to send all this data to sheet. |
| 109 failures.append(line) | 148 self.failures.append(line) |
| 110 count += line.count("successful='True'") | 149 count += line.count("successful='True'") |
| 111 results.close() | 150 results.close() |
| 112 # There is only two tests running for every website: the prompt and | 151 # 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 | 152 # the normal test. If both of the tests were successful, the tests |
| 114 # would be stopped for the current website. | 153 # would be stopped for the current website. |
| 154 print>>sys.stdout, "Test run of %s %s" % (self.test_name, "passed" | |
| 155 if count==2 else "failed") | |
| 115 if count == 2: | 156 if count == 2: |
| 116 return "pass", [] | 157 return True |
| 117 else: | 158 return False |
| 159 | |
| 160 def _run_test(self): | |
| 161 try: | |
| 162 os.remove(self.results_path) | |
| 163 except Exception: | |
|
melandory
2015/02/06 08:37:47
Why don't you want to put line 162 and 166 in same
dvadym
2015/02/06 11:02:26
If the first throws exception, then the second wil
| |
| 118 pass | 164 pass |
| 119 return "fail", failures | 165 try: |
| 120 | 166 os.removedirs(self.profile_path) |
|
vabr (Chromium)
2015/02/05 17:49:57
This is going to try to rmdir all parts of profile
dvadym
2015/02/06 11:02:26
Or great, thanks, it has been here for ages, I did
| |
| 121 | 167 except Exception: |
| 122 def run_tests( | 168 pass |
| 123 chrome_path, chromedriver_path, | 169 self.max_test_runs_left -= 1 |
| 124 profile_path, config_path, *args, **kwargs): | 170 print>>sys.stdout, "Run of test %s started" % self.test_name |
| 125 """ Runs automated tests. | 171 self.runner_process = subprocess.Popen(self.test_cmd) |
| 126 | 172 |
| 127 Args: | 173 def run_tests(config_path): |
| 128 save_path: File, where results of run will be logged. | 174 """ Runs automated tests. """ |
| 129 chrome_path: Location of Chrome binary. | |
| 130 chromedriver_path: Location of Chrome Driver. | |
| 131 passwords_path: Location of file with credentials. | |
| 132 profile_path: Location of profile path. | |
| 133 *args: Variable length argument list. | |
| 134 **kwargs: Arbitrary keyword arguments. | |
| 135 """ | |
| 136 environment = Environment('', '', '', None, False) | 175 environment = Environment('', '', '', None, False) |
| 137 tests.Tests(environment) | 176 tests.Tests(environment) |
| 138 config = ConfigParser.ConfigParser() | 177 config = ConfigParser.ConfigParser() |
| 139 config.read(config_path) | 178 config.read(config_path) |
| 140 date = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') | 179 date = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') |
| 141 try: | 180 max_tests_in_parrallel = config.getint("run_options", "tests_in_parrallel") |
| 142 _, access_token = _authenticate(config.get("credentials", "pkey"), | 181 sheet_writer = SheetWriter(config) |
| 143 config.get("credentials", "client_email")) | 182 full_path = os.path.realpath(__file__) |
| 144 sheet = _spredsheeet_for_logging(config.get("drive", "key"), access_token) | 183 tests_dir = os.path.dirname(full_path) |
| 145 results = tempfile.NamedTemporaryFile( | 184 tests_path = os.path.join(tests_dir, "tests.py") |
| 146 dir=os.path.join(tempfile.gettempdir()), delete=False) | 185 general_test_cmd = ["python", tests_path, "test_name_placeholder", |
| 147 results_path = results.name | 186 "--chrome-path", config.get("binaries", "chrome-path"), |
| 148 results.close() | 187 "--chromedriver-path", config.get("binaries", "chromedriver-path"), |
| 149 | 188 "--passwords-path", config.get("data_files", "passwords_path")] |
| 150 full_path = os.path.realpath(__file__) | 189 runners = [] |
| 151 tests_dir = os.path.dirname(full_path) | 190 tests_to_run = [test.name for test in environment.websitetests] |
| 152 tests_path = os.path.join(tests_dir, "tests.py") | 191 savefile = open(config.get("output", "save-path"), 'w') |
| 153 | 192 if config.has_option("run_options", "tests_to_run"): |
|
melandory
2015/02/06 08:37:47
I would do:
if config.has_option("run_options", "t
dvadym
2015/02/06 11:02:27
I want to try to run only those tests that are cur
vabr (Chromium)
2015/02/06 14:32:52
Validation seems helpful for diagnosing problems,
dvadym
2015/02/06 15:00:16
Done.
| |
| 154 for websitetest in environment.websitetests: | 193 user_selected_tests = config.get("run_options", "tests_to_run").split(',') |
| 155 test_cmd = ["python", tests_path, websitetest.name, | 194 tests_to_run = list(set(tests_to_run) & set(user_selected_tests)) |
| 156 "--chrome-path", chrome_path, | 195 |
| 157 "--chromedriver-path", chromedriver_path, | 196 print>>sys.stdout, "Tests to run %d\nTests: %s" % (len(tests_to_run), |
| 158 "--passwords-path", | 197 tests_to_run) |
| 159 config.get("data_files", "passwords_path"), | 198 while len(runners) + len(tests_to_run) > 0: |
|
vabr (Chromium)
2015/02/05 17:49:58
Isn't runners still empty here?
dvadym
2015/02/06 11:02:26
Before first iteration yes, but then they are adde
vabr (Chromium)
2015/02/06 14:32:52
Sorry, my bad. At the point when I read the end of
dvadym
2015/02/06 15:00:16
No problem :)
| |
| 160 "--profile-path", profile_path, | 199 i = 0 |
| 161 "--save-path", results_path] | 200 while i < len(runners): |
| 162 status, log = _try_run_individual_test(test_cmd, results_path) | 201 result = runners[i].get_test_result() |
| 163 try: | 202 if result != None: # This test run is finished. |
|
melandory
2015/02/06 08:37:46
Can't you do just "if result" here?
dvadym
2015/02/06 11:02:26
Done.
| |
| 164 sheet.InsertRow(sheet.row_count, | 203 status, log = result |
| 165 [websitetest.name, status, date, " | ".join(log)]) | 204 testinfo = [runners[i].test_name, status, date, " | ".join(log)] |
| 166 except Exception: | 205 sheet_writer.write_line_to_sheet(testinfo) |
| 167 pass # TODO(melandory): Sometimes writing to spreadsheet fails. We need | 206 print>>savefile, " ".join(testinfo) |
| 168 # deal with it better that just ignoring. | 207 del runners[i] |
| 169 finally: | 208 else: |
| 170 try: | 209 i += 1 |
| 171 os.remove(results_path) | 210 while len(runners) < max_tests_in_parrallel and len(tests_to_run) > 0: |
| 172 except Exception: | 211 runners.append(TestRunner(general_test_cmd, tests_to_run.pop())) |
| 173 pass | 212 time.sleep(1) # Let us wait for worker process to finish. |
| 174 | 213 savefile.close() |
|
melandory
2015/02/06 08:37:47
Consider using "with".
dvadym
2015/02/06 15:00:16
Done.
| |
| 175 | 214 |
| 176 if __name__ == "__main__": | 215 if __name__ == "__main__": |
| 177 parser = argparse.ArgumentParser( | 216 if len(sys.argv) != 2: |
| 178 description="Password Manager automated tests runner help.") | 217 print "Synopsis:\n python run_tests.py <config_path>" |
| 179 parser.add_argument( | 218 config_path = sys.argv[1] |
| 180 "--chrome-path", action="store", dest="chrome_path", | 219 run_tests(config_path) |
| 181 help="Set the chrome path (required).", required=True) | |
| 182 parser.add_argument( | |
| 183 "--chromedriver-path", action="store", dest="chromedriver_path", | |
| 184 help="Set the chromedriver path (required).", required=True) | |
| 185 parser.add_argument( | |
| 186 "--config-path", action="store", dest="config_path", | |
| 187 help="File with configuration data: drive credentials, password path", | |
| 188 required=True) | |
| 189 parser.add_argument( | |
| 190 "--profile-path", action="store", dest="profile_path", | |
| 191 help="Set the profile path (required). You just need to choose a " | |
| 192 "temporary empty folder. If the folder is not empty all its content " | |
| 193 "is going to be removed.", | |
| 194 required=True) | |
| 195 args = vars(parser.parse_args()) | |
| 196 run_tests(**args) | |
| OLD | NEW |