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 |