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> |
| 14 chromedriver-path=<chrome driver path> |
| 15 [run_options] |
| 16 write_to_sheet=[false|true] |
| 17 tests_in_parrallel=<number of parallel tests> |
| 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 |
| 33 import shutil |
29 import subprocess | 34 import subprocess |
30 import tempfile | 35 import tempfile |
| 36 import time |
| 37 sheet_libraries_import_error = None |
| 38 try: |
| 39 from Sheet import Sheet |
| 40 from apiclient.discovery import build |
| 41 from gdata.gauth import OAuth2TokenFromCredentials |
| 42 from gdata.spreadsheet.service import SpreadsheetsService |
| 43 from oauth2client.client import SignedJwtAssertionCredentials |
| 44 import oauth2client.tools |
| 45 except ImportError as err: |
| 46 sheet_libraries_import_error = err |
| 47 |
31 | 48 |
32 from environment import Environment | 49 from environment import Environment |
33 import tests | 50 import tests |
34 | 51 |
35 _CREDENTIAL_SCOPES = "https://spreadsheets.google.com/feeds" | 52 _CREDENTIAL_SCOPES = "https://spreadsheets.google.com/feeds" |
36 | 53 |
37 # TODO(melandory): Function _authenticate belongs to separate module. | 54 # TODO(dvadym) Change all prints in this file to correspond logging. |
38 def _authenticate(pkey, client_email): | 55 |
39 """ Authenticates user. | 56 # TODO(dvadym) Consider to move this class to separate file. |
40 | 57 class SheetWriter(object): |
41 Args: | 58 |
42 pkey: Full path to file with private key generated by Google | 59 def __init__(self, config): |
43 Developer Console. | 60 self.write_to_sheet = config.getboolean("run_options", "write_to_sheet") |
44 client_email: Email address corresponding to private key and also | 61 if not self.write_to_sheet: |
45 generated by Google Developer Console. | 62 return |
46 """ | 63 if sheet_libraries_import_error: |
47 http, token = None, None | 64 raise sheet_libraries_import_error |
48 with open(pkey) as pkey_file: | 65 self.pkey = config.get("sheet_info", "pkey") |
49 private_key = pkey_file.read() | 66 self.client_mail = config.get("sheet_info", "client_email") |
50 credentials = SignedJwtAssertionCredentials( | 67 self.sheet_key = config.get("sheet_info", "sheet_key") |
51 client_email, private_key, _CREDENTIAL_SCOPES) | 68 _, self.access_token = self._authenticate() |
52 http = httplib2.Http() | 69 self.sheet = self._spredsheeet_for_logging() |
53 http = credentials.authorize(http) | 70 |
54 build('drive', 'v2', http=http) | 71 # TODO(melandory): Function _authenticate belongs to separate module. |
55 token = OAuth2TokenFromCredentials(credentials).access_token | 72 def _authenticate(self): |
56 return http, token | 73 http, token = None, None |
57 | 74 with open(self.pkey) as pkey_file: |
58 # TODO(melandory): Functionality of _spredsheeet_for_logging belongs | 75 private_key = pkey_file.read() |
59 # to websitetests, because this way we do not need to write results of run | 76 credentials = SignedJwtAssertionCredentials( |
60 # in separate file and then read it here. | 77 self.client_email, private_key, _CREDENTIAL_SCOPES) |
61 def _spredsheeet_for_logging(sheet_key, access_token): | 78 http = httplib2.Http() |
62 """ Connects to document where result of test run will be logged. | 79 http = credentials.authorize(http) |
63 Args: | 80 build("drive", "v2", http=http) |
64 sheet_key: Key of sheet in Trix. Can be found in sheet's URL. | 81 token = OAuth2TokenFromCredentials(credentials).access_token |
65 access_token: Access token of an account which should have edit rights. | 82 return http, token |
66 """ | 83 |
67 # Connect to trix | 84 # TODO(melandory): Functionality of _spredsheeet_for_logging belongs |
68 service = SpreadsheetsService(additional_headers={ | 85 # to websitetests, because this way we do not need to write results of run |
69 "Authorization": "Bearer " + access_token}) | 86 # in separate file and then read it here. |
70 sheet = Sheet(service, sheet_key) | 87 def _spredsheeet_for_logging(self): |
71 return sheet | 88 """ Connects to document where result of test run will be logged. """ |
72 | 89 # Connect to trix |
73 def _try_run_individual_test(test_cmd, results_path): | 90 service = SpreadsheetsService(additional_headers={ |
74 """ Runs individual test and logs results to trix. | 91 "Authorization": "Bearer " + self.access_token}) |
75 | 92 sheet = Sheet(service, self.sheet_key) |
76 Args: | 93 return sheet |
77 test_cmd: String contains command which runs test. | 94 |
78 results_path: Full path to file where results of test run will be logged. | 95 def write_line_to_sheet(self, data): |
79 """ | 96 if not self.write_to_sheet: |
80 failures = [] | 97 return |
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: | 98 try: |
92 os.remove(results_path) | 99 self.sheet.InsertRow(self.sheet.row_count, data) |
93 except Exception: | 100 except Exception: |
94 pass | 101 pass # TODO(melandory): Sometimes writing to spreadsheet fails. We need |
| 102 # to deal with it better that just ignoring it. |
| 103 |
| 104 class TestRunner(object): |
| 105 |
| 106 def __init__(self, general_test_cmd, test_name): |
| 107 """ Args: |
| 108 general_test_cmd: String contains part of run command common for all tests, |
| 109 [2] is placeholder for test name. |
| 110 test_name: Test name (facebook for example). |
| 111 """ |
| 112 self.profile_path = tempfile.mkdtemp() |
| 113 results = tempfile.NamedTemporaryFile(delete=False) |
| 114 self.results_path = results.name |
| 115 results.close() |
| 116 self.test_cmd = general_test_cmd + ["--profile-path", self.profile_path, |
| 117 "--save-path", self.results_path] |
| 118 self.test_cmd[2] = self.test_name = test_name |
95 # TODO(rchtara): Using "timeout is just temporary until a better, | 119 # TODO(rchtara): Using "timeout is just temporary until a better, |
96 # platform-independent solution is found. | 120 # platform-independent solution is found. |
97 | |
98 # The website test runs in two passes, each pass has an internal | 121 # The website test runs in two passes, each pass has an internal |
99 # timeout of 200s for waiting (see |remaining_time_to_wait| and | 122 # timeout of 200s for waiting (see |remaining_time_to_wait| and |
100 # Wait() in websitetest.py). Accounting for some more time spent on | 123 # Wait() in websitetest.py). Accounting for some more time spent on |
101 # the non-waiting execution, 300 seconds should be the upper bound on | 124 # 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. | 125 # the runtime of one pass, thus 600 seconds for the whole test. |
103 subprocess.call(["timeout", "600"] + test_cmd) | 126 self.test_cmd = ["timeout", "600"] + self.test_cmd |
104 if os.path.isfile(results_path): | 127 self.runner_process = None |
105 results = open(results_path, "r") | 128 # The tests can be flaky. This is why we try to rerun up to 3 times. |
106 count = 0 # Count the number of successful tests. | 129 self.max_test_runs_left = 3 |
| 130 self.failures = [] |
| 131 self._run_test() |
| 132 |
| 133 def get_test_result(self): |
| 134 """ Return None if result is not ready yet.""" |
| 135 test_running = self.runner_process and self.runner_process.poll() is None |
| 136 if test_running: return None |
| 137 # Test is not running, now we have to check if we want to start it again. |
| 138 if self._check_if_test_passed(): |
| 139 print "Test " + self.test_name + " passed" |
| 140 return "pass", [] |
| 141 if self.max_test_runs_left == 0: |
| 142 print "Test " + self.test_name + " failed" |
| 143 return "fail", self.failures |
| 144 self._run_test() |
| 145 return None |
| 146 |
| 147 def _check_if_test_passed(self): |
| 148 if os.path.isfile(self.results_path): |
| 149 results = open(self.results_path, "r") |
| 150 count = 0 # Count the number of successful tests. |
107 for line in results: | 151 for line in results: |
108 # TODO(melandory): We do not need to send all this data to sheet. | 152 # TODO(melandory): We do not need to send all this data to sheet. |
109 failures.append(line) | 153 self.failures.append(line) |
110 count += line.count("successful='True'") | 154 count += line.count("successful='True'") |
111 results.close() | 155 results.close() |
112 # There is only two tests running for every website: the prompt and | 156 # 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 | 157 # the normal test. If both of the tests were successful, the tests |
114 # would be stopped for the current website. | 158 # would be stopped for the current website. |
| 159 print "Test run of %s %s" % (self.test_name, "passed" |
| 160 if count == 2 else "failed") |
115 if count == 2: | 161 if count == 2: |
116 return "pass", [] | 162 return True |
117 else: | 163 return False |
| 164 |
| 165 def _run_test(self): |
| 166 """Run separate process that once run test for one site.""" |
| 167 try: |
| 168 os.remove(self.results_path) |
| 169 except Exception: |
118 pass | 170 pass |
119 return "fail", failures | 171 try: |
120 | 172 shutil.rmtree(self.profile_path) |
121 | 173 except Exception: |
122 def run_tests( | 174 pass |
123 chrome_path, chromedriver_path, | 175 self.max_test_runs_left -= 1 |
124 profile_path, config_path, *args, **kwargs): | 176 print "Run of test %s started" % self.test_name |
125 """ Runs automated tests. | 177 self.runner_process = subprocess.Popen(self.test_cmd) |
126 | 178 |
127 Args: | 179 def run_tests(config_path): |
128 save_path: File, where results of run will be logged. | 180 """ Runs automated tests. """ |
129 chrome_path: Location of Chrome binary. | 181 environment = Environment("", "", "", None, False) |
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) | |
137 tests.Tests(environment) | 182 tests.Tests(environment) |
138 config = ConfigParser.ConfigParser() | 183 config = ConfigParser.ConfigParser() |
139 config.read(config_path) | 184 config.read(config_path) |
140 date = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') | 185 date = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') |
141 try: | 186 max_tests_in_parrallel = config.getint("run_options", "tests_in_parrallel") |
142 _, access_token = _authenticate(config.get("credentials", "pkey"), | 187 sheet_writer = SheetWriter(config) |
143 config.get("credentials", "client_email")) | 188 full_path = os.path.realpath(__file__) |
144 sheet = _spredsheeet_for_logging(config.get("drive", "key"), access_token) | 189 tests_dir = os.path.dirname(full_path) |
145 results = tempfile.NamedTemporaryFile( | 190 tests_path = os.path.join(tests_dir, "tests.py") |
146 dir=os.path.join(tempfile.gettempdir()), delete=False) | 191 general_test_cmd = ["python", tests_path, "test_name_placeholder", |
147 results_path = results.name | 192 "--chrome-path", config.get("binaries", "chrome-path"), |
148 results.close() | 193 "--chromedriver-path", config.get("binaries", "chromedriver-path"), |
149 | 194 "--passwords-path", config.get("data_files", "passwords_path")] |
150 full_path = os.path.realpath(__file__) | 195 runners = [] |
151 tests_dir = os.path.dirname(full_path) | 196 tests_to_run = [test.name for test in environment.websitetests] |
152 tests_path = os.path.join(tests_dir, "tests.py") | 197 if config.has_option("run_options", "tests_to_run"): |
153 | 198 user_selected_tests = config.get("run_options", "tests_to_run").split(',') |
154 for websitetest in environment.websitetests: | 199 # TODO((dvadym) Validate the user selected tests are available. |
155 test_cmd = ["python", tests_path, websitetest.name, | 200 tests_to_run = list(set(tests_to_run) & set(user_selected_tests)) |
156 "--chrome-path", chrome_path, | 201 |
157 "--chromedriver-path", chromedriver_path, | 202 with open(config.get("output", "save-path"), 'w') as savefile: |
158 "--passwords-path", | 203 print "Tests to run %d\nTests: %s" % (len(tests_to_run), tests_to_run) |
159 config.get("data_files", "passwords_path"), | 204 while len(runners) + len(tests_to_run) > 0: |
160 "--profile-path", profile_path, | 205 i = 0 |
161 "--save-path", results_path] | 206 while i < len(runners): |
162 status, log = _try_run_individual_test(test_cmd, results_path) | 207 result = runners[i].get_test_result() |
163 try: | 208 if result: # This test run is finished. |
164 sheet.InsertRow(sheet.row_count, | 209 status, log = result |
165 [websitetest.name, status, date, " | ".join(log)]) | 210 testinfo = [runners[i].test_name, status, date, " | ".join(log)] |
166 except Exception: | 211 sheet_writer.write_line_to_sheet(testinfo) |
167 pass # TODO(melandory): Sometimes writing to spreadsheet fails. We need | 212 print>>savefile, " ".join(testinfo) |
168 # deal with it better that just ignoring. | 213 del runners[i] |
169 finally: | 214 else: |
170 try: | 215 i += 1 |
171 os.remove(results_path) | 216 while len(runners) < max_tests_in_parrallel and len(tests_to_run) > 0: |
172 except Exception: | 217 runners.append(TestRunner(general_test_cmd, tests_to_run.pop())) |
173 pass | 218 time.sleep(1) # Let us wait for worker process to finish. |
174 | |
175 | 219 |
176 if __name__ == "__main__": | 220 if __name__ == "__main__": |
177 parser = argparse.ArgumentParser( | 221 if len(sys.argv) != 2: |
178 description="Password Manager automated tests runner help.") | 222 print "Synopsis:\n python run_tests.py <config_path>" |
179 parser.add_argument( | 223 config_path = sys.argv[1] |
180 "--chrome-path", action="store", dest="chrome_path", | 224 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 |