OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 | 2 |
3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. | 3 # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. |
4 # Use of this source code is governed by a BSD-style license that can be | 4 # Use of this source code is governed by a BSD-style license that can be |
5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
6 | 6 |
| 7 """This module runs a suite of Auto Update tests. |
| 8 |
| 9 The tests can be run on either a virtual machine or actual device depending |
| 10 on parameters given. Specific tests can be run by invoking --test_prefix. |
| 11 Verbose is useful for many of the tests if you want to see individual commands |
| 12 being run during the update process. |
| 13 """ |
| 14 |
7 import optparse | 15 import optparse |
8 import os | 16 import os |
9 import re | 17 import re |
10 import sys | 18 import sys |
11 import thread | 19 import thread |
12 import time | 20 import time |
13 import unittest | 21 import unittest |
14 import urllib | 22 import urllib |
15 | 23 |
16 sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) | 24 sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) |
17 from cros_build_lib import Die | 25 from cros_build_lib import Die |
18 from cros_build_lib import Info | 26 from cros_build_lib import Info |
19 from cros_build_lib import ReinterpretPathForChroot | 27 from cros_build_lib import ReinterpretPathForChroot |
20 from cros_build_lib import RunCommand | 28 from cros_build_lib import RunCommand |
21 from cros_build_lib import RunCommandCaptureOutput | 29 from cros_build_lib import RunCommandCaptureOutput |
22 from cros_build_lib import Warning | 30 from cros_build_lib import Warning |
23 | 31 |
24 import cros_test_proxy | 32 import cros_test_proxy |
25 | 33 |
26 # VM Constants. | |
27 _FULL_VDISK_SIZE = 6072 | |
28 _FULL_STATEFULFS_SIZE = 3074 | |
29 _KVM_PID_FILE = '/tmp/harness_pid' | |
30 _VERIFY_SUITE = 'suite_Smoke' | |
31 | |
32 # Globals to communicate options to unit tests. | |
33 global base_image_path | |
34 global board | |
35 global remote | |
36 global target_image_path | |
37 global vm_graphics_flag | |
38 | 34 |
39 class UpdateException(Exception): | 35 class UpdateException(Exception): |
40 """Exception thrown when UpdateImage or UpdateUsingPayload fail""" | 36 """Exception thrown when _UpdateImage or _UpdateUsingPayload fail""" |
41 def __init__(self, code, stdout): | 37 def __init__(self, code, stdout): |
42 self.code = code | 38 self.code = code |
43 self.stdout = stdout | 39 self.stdout = stdout |
44 | 40 |
| 41 |
45 class AUTest(object): | 42 class AUTest(object): |
46 """Abstract interface that defines an Auto Update test.""" | 43 """Abstract interface that defines an Auto Update test.""" |
47 source_image = '' | |
48 use_delta_updates = False | |
49 verbose = False | 44 verbose = False |
50 | 45 |
51 def setUp(self): | 46 def setUp(self): |
52 unittest.TestCase.setUp(self) | 47 unittest.TestCase.setUp(self) |
53 # Set these up as they are used often. | 48 # Set these up as they are used often. |
54 self.crosutils = os.path.join(os.path.dirname(__file__), '..') | 49 self.crosutils = os.path.join(os.path.dirname(__file__), '..') |
55 self.crosutilsbin = os.path.join(os.path.dirname(__file__)) | 50 self.crosutilsbin = os.path.join(os.path.dirname(__file__)) |
56 self.download_folder = os.path.join(self.crosutils, 'latest_download') | 51 self.download_folder = os.path.join(self.crosutils, 'latest_download') |
57 if not os.path.exists(self.download_folder): | 52 if not os.path.exists(self.download_folder): |
58 os.makedirs(self.download_folder) | 53 os.makedirs(self.download_folder) |
59 | 54 |
| 55 # -------- Helper functions --------- |
| 56 |
60 def GetStatefulChangeFlag(self, stateful_change): | 57 def GetStatefulChangeFlag(self, stateful_change): |
61 """Returns the flag to pass to image_to_vm for the stateful change.""" | 58 """Returns the flag to pass to image_to_vm for the stateful change.""" |
62 stateful_change_flag = '' | 59 stateful_change_flag = '' |
63 if stateful_change: | 60 if stateful_change: |
64 stateful_change_flag = '--stateful_update_flag=%s' % stateful_change | 61 stateful_change_flag = '--stateful_update_flag=%s' % stateful_change |
65 | 62 |
66 return stateful_change_flag | 63 return stateful_change_flag |
67 | 64 |
68 def ParseGenerateTestReportOutput(self, output): | 65 def _ParseGenerateTestReportOutput(self, output): |
69 """Returns the percentage of tests that passed based on output.""" | 66 """Returns the percentage of tests that passed based on output.""" |
70 percent_passed = 0 | 67 percent_passed = 0 |
71 lines = output.split('\n') | 68 lines = output.split('\n') |
72 | 69 |
73 for line in lines: | 70 for line in lines: |
74 if line.startswith("Total PASS:"): | 71 if line.startswith("Total PASS:"): |
75 # FORMAT: ^TOTAL PASS: num_passed/num_total (percent%)$ | 72 # FORMAT: ^TOTAL PASS: num_passed/num_total (percent%)$ |
76 percent_passed = line.split()[3].strip('()%') | 73 percent_passed = line.split()[3].strip('()%') |
77 Info('Percent of tests passed %s' % percent_passed) | 74 Info('Percent of tests passed %s' % percent_passed) |
78 break | 75 break |
79 | 76 |
80 return int(percent_passed) | 77 return int(percent_passed) |
81 | 78 |
82 # TODO(sosa) - Remove try and convert function to DeltaUpdateImage(). | 79 def AssertEnoughTestsPassed(self, unittest, output, percent_required_to_pass): |
83 def TryDeltaAndFallbackToFull(self, src_image, image, stateful_change='old'): | 80 """Helper function that asserts a sufficient number of tests passed. |
84 """Tries the delta update first if set and falls back to full update.""" | |
85 if self.use_delta_updates: | |
86 try: | |
87 self.source_image = src_image | |
88 self._UpdateImageReportError(image, stateful_change) | |
89 except: | |
90 Warning('Delta update failed, disabling delta updates and retrying.') | |
91 self.use_delta_updates = False | |
92 self.source_image = '' | |
93 self._UpdateImageReportError(image, stateful_change) | |
94 else: | |
95 self._UpdateImageReportError(image, stateful_change) | |
96 | 81 |
97 def _UpdateImageReportError(self, image_path, stateful_change='old', | 82 Args: |
98 proxy_port=None): | 83 unittest: Handle to the unittest. |
99 """Calls UpdateImage and reports any error to the console. | 84 output: stdout from a test run. |
| 85 percent_required_to_pass: percentage required to pass. This should be |
| 86 fall between 0-100. |
| 87 Returns: |
| 88 percent that passed. |
| 89 """ |
| 90 Info('Output from VerifyImage():') |
| 91 print >> sys.stderr, output |
| 92 sys.stderr.flush() |
| 93 percent_passed = self._ParseGenerateTestReportOutput(output) |
| 94 Info('Percent passed: %d vs. Percent required: %d' % ( |
| 95 percent_passed, percent_required_to_pass)) |
| 96 unittest.assertTrue(percent_passed >= percent_required_to_pass) |
| 97 return percent_passed |
100 | 98 |
101 Still throws the exception. | 99 def PerformUpdate(self, image_path, src_image_path='', stateful_change='old', |
| 100 proxy_port=None): |
| 101 """Performs an update using _UpdateImage and reports any error. |
| 102 |
| 103 Subclasses should not override this method but override _UpdateImage |
| 104 instead. |
| 105 |
| 106 Args: |
| 107 image_path: Path to the image to update with. This image must be a test |
| 108 image. |
| 109 src_image_path: Optional. If set, perform a delta update using the |
| 110 image specified by the path as the source image. |
| 111 stateful_change: How to modify the stateful partition. Values are: |
| 112 'old': Don't modify stateful partition. Just update normally. |
| 113 'clean': Uses clobber-state to wipe the stateful partition with the |
| 114 exception of code needed for ssh. |
| 115 proxy_port: Port to have the client connect to. For use with |
| 116 CrosTestProxy. |
| 117 Raises an UpdateException if _UpdateImage returns an error. |
102 """ | 118 """ |
103 try: | 119 try: |
104 self.UpdateImage(image_path, stateful_change, proxy_port) | 120 if not self.use_delta_updates: |
| 121 src_image_path = '' |
| 122 |
| 123 self._UpdateImage(image_path, src_image_path, stateful_change, proxy_port) |
105 except UpdateException as err: | 124 except UpdateException as err: |
106 # If the update fails, print it out | 125 # If the update fails, print it out |
107 Warning(err.stdout) | 126 Warning(err.stdout) |
108 raise | 127 raise |
109 | 128 |
110 def _AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): | 129 def AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): |
111 """Attempt a payload update, expect it to fail with expected log""" | 130 """Attempt a payload update, expect it to fail with expected log""" |
112 try: | 131 try: |
113 self.UpdateUsingPayload(payload) | 132 self._UpdateUsingPayload(payload) |
114 except UpdateException as err: | 133 except UpdateException as err: |
115 # Will raise ValueError if expected is not found. | 134 # Will raise ValueError if expected is not found. |
116 if re.search(re.escape(expected_msg), err.stdout, re.MULTILINE): | 135 if re.search(re.escape(expected_msg), err.stdout, re.MULTILINE): |
117 return | 136 return |
118 | 137 |
119 Warning("Didn't find '%s' in:" % expected_msg) | 138 Warning("Didn't find '%s' in:" % expected_msg) |
120 Warning(err.stdout) | 139 Warning(err.stdout) |
121 self.fail('We managed to update when failure was expected') | 140 self.fail('We managed to update when failure was expected') |
122 | 141 |
123 def _AttemptUpdateWithFilter(self, filter): | 142 def AttemptUpdateWithFilter(self, filter): |
124 """Update through a proxy, with a specified filter, and expect success.""" | 143 """Update through a proxy, with a specified filter, and expect success.""" |
125 | 144 |
126 self.PrepareBase(target_image_path) | 145 self.PrepareBase(self.target_image_path) |
127 | 146 |
128 # The devserver runs at port 8080 by default. We assume that here, and | 147 # The devserver runs at port 8080 by default. We assume that here, and |
129 # start our proxy at 8081. We then tell our update tools to have the | 148 # start our proxy at 8081. We then tell our update tools to have the |
130 # client connect to 8081 instead of 8080. | 149 # client connect to 8081 instead of 8080. |
131 proxy_port = 8081 | 150 proxy_port = 8081 |
132 proxy = cros_test_proxy.CrosTestProxy(port_in=proxy_port, | 151 proxy = cros_test_proxy.CrosTestProxy(port_in=proxy_port, |
133 address_out='127.0.0.1', | 152 address_out='127.0.0.1', |
134 port_out=8080, | 153 port_out=8080, |
135 filter=filter) | 154 filter=filter) |
136 proxy.serve_forever_in_thread() | 155 proxy.serve_forever_in_thread() |
137 | 156 |
138 # This update is expected to fail... | 157 # This update is expected to fail... |
139 try: | 158 try: |
140 self._UpdateImageReportError(target_image_path, proxy_port=proxy_port) | 159 self.PerformUpdate(self.target_image_path, self.base_image_path, |
| 160 proxy_port=proxy_port) |
141 finally: | 161 finally: |
142 proxy.shutdown() | 162 proxy.shutdown() |
143 | 163 |
| 164 # -------- Functions that subclasses should override --------- |
| 165 |
| 166 @classmethod |
| 167 def ProcessOptions(cls, parser, options): |
| 168 """Processes options. |
| 169 |
| 170 Static method that should be called from main. Subclasses should also |
| 171 call their parent method if they override it. |
| 172 """ |
| 173 cls.verbose = options.verbose |
| 174 cls.base_image_path = options.base_image |
| 175 cls.target_image_path = options.target_image |
| 176 cls.use_delta_updates = options.delta |
| 177 if options.quick_test: |
| 178 cls.verify_suite = 'build_RootFilesystemSize' |
| 179 else: |
| 180 cls.verify_suite = 'suite_Smoke' |
| 181 |
| 182 # Sanity checks. |
| 183 if not cls.base_image_path: |
| 184 parser.error('Need path to base image for vm.') |
| 185 elif not os.path.exists(cls.base_image_path): |
| 186 Die('%s does not exist' % cls.base_image_path) |
| 187 |
| 188 if not cls.target_image_path: |
| 189 parser.error('Need path to target image to update with.') |
| 190 elif not os.path.exists(cls.target_image_path): |
| 191 Die('%s does not exist' % cls.target_image_path) |
| 192 |
144 def PrepareBase(self, image_path): | 193 def PrepareBase(self, image_path): |
145 """Prepares target with base_image_path.""" | 194 """Prepares target with base_image_path.""" |
146 pass | 195 pass |
147 | 196 |
148 def UpdateImage(self, image_path, stateful_change='old', proxy_port=None): | 197 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', |
149 """Updates target with the image given by the image_path. | 198 proxy_port=None): |
| 199 """Implementation of an actual update. |
150 | 200 |
151 Args: | 201 See PerformUpdate for description of args. Subclasses must override this |
152 image_path: Path to the image to update with. This image must be a test | 202 method with the correct update procedure for the class. |
153 image. | |
154 stateful_change: How to modify the stateful partition. Values are: | |
155 'old': Don't modify stateful partition. Just update normally. | |
156 'clean': Uses clobber-state to wipe the stateful partition with the | |
157 exception of code needed for ssh. | |
158 proxy_port: Port to have the client connect to. For use with | |
159 CrosTestProxy. | |
160 """ | 203 """ |
161 pass | 204 pass |
162 | 205 |
163 def UpdateUsingPayload(self, | 206 def _UpdateUsingPayload(self, update_path, stateful_change='old', |
164 update_path, | |
165 stateful_change='old', | |
166 proxy_port=None): | 207 proxy_port=None): |
167 """Updates target with the pre-generated update stored in update_path | 208 """Updates target with the pre-generated update stored in update_path. |
| 209 |
| 210 Subclasses must override this method with the correct update procedure for |
| 211 the class. |
168 | 212 |
169 Args: | 213 Args: |
170 update_path: Path to the image to update with. This directory should | 214 update_path: Path to the image to update with. This directory should |
171 contain both update.gz, and stateful.image.gz | 215 contain both update.gz, and stateful.image.gz |
172 proxy_port: Port to have the client connect to. For use with | 216 proxy_port: Port to have the client connect to. For use with |
173 CrosTestProxy. | 217 CrosTestProxy. |
174 """ | 218 """ |
175 pass | 219 pass |
176 | 220 |
177 def VerifyImage(self, percent_required_to_pass): | 221 def VerifyImage(self, percent_required_to_pass): |
178 """Verifies the image with tests. | 222 """Verifies the image with tests. |
179 | 223 |
180 Verifies that the test images passes the percent required. | 224 Verifies that the test images passes the percent required. Subclasses must |
| 225 override this method with the correct update procedure for the class. |
181 | 226 |
182 Args: | 227 Args: |
183 percent_required_to_pass: percentage required to pass. This should be | 228 percent_required_to_pass: percentage required to pass. This should be |
184 fall between 0-100. | 229 fall between 0-100. |
185 | 230 |
186 Returns: | 231 Returns: |
187 Returns the percent that passed. | 232 Returns the percent that passed. |
188 """ | 233 """ |
189 pass | 234 pass |
190 | 235 |
191 def CommonVerifyImage(self, unittest, output, percent_required_to_pass): | 236 # -------- Tests --------- |
192 """Helper function for VerifyImage that returns percent of tests passed. | |
193 | 237 |
194 Takes output from a test suite, verifies the number of tests passed is | 238 def testUpdateKeepStateful(self): |
195 sufficient and outputs info. | |
196 | |
197 Args: | |
198 unittest: Handle to the unittest. | |
199 output: stdout from a test run. | |
200 percent_required_to_pass: percentage required to pass. This should be | |
201 fall between 0-100. | |
202 Returns: | |
203 percent that passed. | |
204 """ | |
205 Info('Output from VerifyImage():') | |
206 print >> sys.stderr, output | |
207 sys.stderr.flush() | |
208 percent_passed = self.ParseGenerateTestReportOutput(output) | |
209 Info('Percent passed: %d vs. Percent required: %d' % ( | |
210 percent_passed, percent_required_to_pass)) | |
211 unittest.assertTrue(percent_passed >= | |
212 percent_required_to_pass) | |
213 return percent_passed | |
214 | |
215 def testFullUpdateKeepStateful(self): | |
216 """Tests if we can update normally. | 239 """Tests if we can update normally. |
217 | 240 |
218 This test checks that we can update by updating the stateful partition | 241 This test checks that we can update by updating the stateful partition |
219 rather than wiping it. | 242 rather than wiping it. |
220 """ | 243 """ |
221 # Just make sure some tests pass on original image. Some old images | 244 # Just make sure some tests pass on original image. Some old images |
222 # don't pass many tests. | 245 # don't pass many tests. |
223 self.PrepareBase(base_image_path) | 246 self.PrepareBase(self.base_image_path) |
224 # TODO(sosa): move to 100% once we start testing using the autotest paired | 247 # TODO(sosa): move to 100% once we start testing using the autotest paired |
225 # with the dev channel. | 248 # with the dev channel. |
226 percent_passed = self.VerifyImage(10) | 249 percent_passed = self.VerifyImage(10) |
227 | 250 |
228 # Update to - all tests should pass on new image. | 251 # Update to - all tests should pass on new image. |
229 Info('Updating from base image on vm to target image.') | 252 Info('Updating from base image on vm to target image.') |
230 self.TryDeltaAndFallbackToFull(base_image_path, target_image_path) | 253 self.PerformUpdate(self.target_image_path, self.base_image_path) |
231 self.VerifyImage(100) | 254 self.VerifyImage(100) |
232 | 255 |
233 # Update from - same percentage should pass that originally passed. | 256 # Update from - same percentage should pass that originally passed. |
234 Info('Updating from updated image on vm back to base image.') | 257 Info('Updating from updated image on vm back to base image.') |
235 self.TryDeltaAndFallbackToFull(target_image_path, base_image_path) | 258 self.PerformUpdate(self.base_image_path, self.target_image_path) |
236 self.VerifyImage(percent_passed) | 259 self.VerifyImage(percent_passed) |
237 | 260 |
238 def testFullUpdateWipeStateful(self): | 261 def testUpdateWipeStateful(self): |
239 """Tests if we can update after cleaning the stateful partition. | 262 """Tests if we can update after cleaning the stateful partition. |
240 | 263 |
241 This test checks that we can update successfully after wiping the | 264 This test checks that we can update successfully after wiping the |
242 stateful partition. | 265 stateful partition. |
243 """ | 266 """ |
244 # Just make sure some tests pass on original image. Some old images | 267 # Just make sure some tests pass on original image. Some old images |
245 # don't pass many tests. | 268 # don't pass many tests. |
246 self.PrepareBase(base_image_path) | 269 self.PrepareBase(self.base_image_path) |
247 # TODO(sosa): move to 100% once we start testing using the autotest paired | 270 # TODO(sosa): move to 100% once we start testing using the autotest paired |
248 # with the dev channel. | 271 # with the dev channel. |
249 percent_passed = self.VerifyImage(10) | 272 percent_passed = self.VerifyImage(10) |
250 | 273 |
251 # Update to - all tests should pass on new image. | 274 # Update to - all tests should pass on new image. |
252 Info('Updating from base image on vm to target image and wiping stateful.') | 275 Info('Updating from base image on vm to target image and wiping stateful.') |
253 self.TryDeltaAndFallbackToFull(base_image_path, target_image_path, 'clean') | 276 self.PerformUpdate(self.target_image_path, self.base_image_path, 'clean') |
254 self.VerifyImage(100) | 277 self.VerifyImage(100) |
255 | 278 |
256 # Update from - same percentage should pass that originally passed. | 279 # Update from - same percentage should pass that originally passed. |
257 Info('Updating from updated image back to base image and wiping stateful.') | 280 Info('Updating from updated image back to base image and wiping stateful.') |
258 self.TryDeltaAndFallbackToFull(target_image_path, base_image_path, 'clean') | 281 self.PerformUpdate(self.base_image_path, self.target_image_path, 'clean') |
259 self.VerifyImage(percent_passed) | 282 self.VerifyImage(percent_passed) |
260 | 283 |
261 def testPartialUpdate(self): | 284 def testPartialUpdate(self): |
262 """Tests what happens if we attempt to update with a truncated payload.""" | 285 """Tests what happens if we attempt to update with a truncated payload.""" |
263 # Preload with the version we are trying to test. | 286 # Preload with the version we are trying to test. |
264 self.PrepareBase(target_image_path) | 287 self.PrepareBase(self.target_image_path) |
265 | 288 |
266 # Image can be updated at: | 289 # Image can be updated at: |
267 # ~chrome-eng/chromeos/localmirror/autest-images | 290 # ~chrome-eng/chromeos/localmirror/autest-images |
268 url = 'http://gsdview.appspot.com/chromeos-localmirror/' \ | 291 url = 'http://gsdview.appspot.com/chromeos-localmirror/' \ |
269 'autest-images/truncated_image.gz' | 292 'autest-images/truncated_image.gz' |
270 payload = os.path.join(self.download_folder, 'truncated_image.gz') | 293 payload = os.path.join(self.download_folder, 'truncated_image.gz') |
271 | 294 |
272 # Read from the URL and write to the local file | 295 # Read from the URL and write to the local file |
273 urllib.urlretrieve(url, payload) | 296 urllib.urlretrieve(url, payload) |
274 | 297 |
275 expected_msg = 'download_hash_data == update_check_response_hash failed' | 298 expected_msg = 'download_hash_data == update_check_response_hash failed' |
276 self._AttemptUpdateWithPayloadExpectedFailure(payload, expected_msg) | 299 self.AttemptUpdateWithPayloadExpectedFailure(payload, expected_msg) |
277 | 300 |
278 def testCorruptedUpdate(self): | 301 def testCorruptedUpdate(self): |
279 """Tests what happens if we attempt to update with a corrupted payload.""" | 302 """Tests what happens if we attempt to update with a corrupted payload.""" |
280 # Preload with the version we are trying to test. | 303 # Preload with the version we are trying to test. |
281 self.PrepareBase(target_image_path) | 304 self.PrepareBase(self.target_image_path) |
282 | 305 |
283 # Image can be updated at: | 306 # Image can be updated at: |
284 # ~chrome-eng/chromeos/localmirror/autest-images | 307 # ~chrome-eng/chromeos/localmirror/autest-images |
285 url = 'http://gsdview.appspot.com/chromeos-localmirror/' \ | 308 url = 'http://gsdview.appspot.com/chromeos-localmirror/' \ |
286 'autest-images/corrupted_image.gz' | 309 'autest-images/corrupted_image.gz' |
287 payload = os.path.join(self.download_folder, 'corrupted.gz') | 310 payload = os.path.join(self.download_folder, 'corrupted.gz') |
288 | 311 |
289 # Read from the URL and write to the local file | 312 # Read from the URL and write to the local file |
290 urllib.urlretrieve(url, payload) | 313 urllib.urlretrieve(url, payload) |
291 | 314 |
292 # This update is expected to fail... | 315 # This update is expected to fail... |
293 expected_msg = 'zlib inflate() error:-3' | 316 expected_msg = 'zlib inflate() error:-3' |
294 self._AttemptUpdateWithPayloadExpectedFailure(payload, expected_msg) | 317 self.AttemptUpdateWithPayloadExpectedFailure(payload, expected_msg) |
295 | 318 |
296 def testInterruptedUpdate(self): | 319 def testInterruptedUpdate(self): |
297 """Tests what happens if we interrupt payload delivery 3 times.""" | 320 """Tests what happens if we interrupt payload delivery 3 times.""" |
298 | 321 |
299 class InterruptionFilter(cros_test_proxy.Filter): | 322 class InterruptionFilter(cros_test_proxy.Filter): |
300 """This filter causes the proxy to interrupt the download 3 times | 323 """This filter causes the proxy to interrupt the download 3 times |
301 | 324 |
302 It does this by closing the first three connections to transfer | 325 It does this by closing the first three connections to transfer |
303 2M total in the outbound connection after they transfer the | 326 2M total in the outbound connection after they transfer the |
304 2M. | 327 2M. |
(...skipping 13 matching lines...) Expand all Loading... |
318 outbound will be closed. | 341 outbound will be closed. |
319 """ | 342 """ |
320 if self.close_count < 3: | 343 if self.close_count < 3: |
321 if self.data_size > (2 * 1024 * 1024): | 344 if self.data_size > (2 * 1024 * 1024): |
322 self.close_count += 1 | 345 self.close_count += 1 |
323 return None | 346 return None |
324 | 347 |
325 self.data_size += len(data) | 348 self.data_size += len(data) |
326 return data | 349 return data |
327 | 350 |
328 self._AttemptUpdateWithFilter(InterruptionFilter()) | 351 self.AttemptUpdateWithFilter(InterruptionFilter()) |
329 | 352 |
330 def testDelayedUpdate(self): | 353 def testDelayedUpdate(self): |
331 """Tests what happens if some data is delayed during update delivery""" | 354 """Tests what happens if some data is delayed during update delivery""" |
332 | 355 |
333 class DelayedFilter(cros_test_proxy.Filter): | 356 class DelayedFilter(cros_test_proxy.Filter): |
334 """Causes intermittent delays in data transmission. | 357 """Causes intermittent delays in data transmission. |
335 | 358 |
336 It does this by inserting 3 20 second delays when transmitting | 359 It does this by inserting 3 20 second delays when transmitting |
337 data after 2M has been sent. | 360 data after 2M has been sent. |
338 """ | 361 """ |
339 def setup(self): | 362 def setup(self): |
340 """Called once at the start of each connection.""" | 363 """Called once at the start of each connection.""" |
341 self.data_size = 0 | 364 self.data_size = 0 |
342 self.delay_count = 0 | 365 self.delay_count = 0 |
343 | 366 |
344 def OutBound(self, data): | 367 def OutBound(self, data): |
345 """Called once per packet for outgoing data. | 368 """Called once per packet for outgoing data. |
346 | 369 |
347 The first three packets after we reach 2M transferred | 370 The first three packets after we reach 2M transferred |
348 are delayed by 20 seconds. | 371 are delayed by 20 seconds. |
349 """ | 372 """ |
350 if self.delay_count < 3: | 373 if self.delay_count < 3: |
351 if self.data_size > (2 * 1024 * 1024): | 374 if self.data_size > (2 * 1024 * 1024): |
352 self.delay_count += 1 | 375 self.delay_count += 1 |
353 time.sleep(20) | 376 time.sleep(20) |
354 | 377 |
355 self.data_size += len(data) | 378 self.data_size += len(data) |
356 return data | 379 return data |
357 | 380 |
358 self._AttemptUpdateWithFilter(DelayedFilter()) | 381 self.AttemptUpdateWithFilter(DelayedFilter()) |
359 | 382 |
360 def SimpleTest(self): | 383 def SimpleTest(self): |
361 """A simple update that updates once from a base image to a target. | 384 """A simple update that updates once from a base image to a target. |
362 | 385 |
363 We explicitly don't use test prefix so that isn't run by default. Can be | 386 We explicitly don't use test prefix so that isn't run by default. Can be |
364 run using test_prefix option. | 387 run using test_prefix option. |
365 """ | 388 """ |
366 self.PrepareBase(base_image_path) | 389 self.PrepareBase(self.base_image_path) |
367 self.UpdateImage(target_image_path) | 390 self.PerformUpdate(self.target_image_path, self.base_image_path) |
368 self.VerifyImage(100) | 391 self.VerifyImage(100) |
369 | 392 |
370 | 393 |
371 class RealAUTest(unittest.TestCase, AUTest): | 394 class RealAUTest(unittest.TestCase, AUTest): |
372 """Test harness for updating real images.""" | 395 """Test harness for updating real images.""" |
373 | 396 |
374 def setUp(self): | 397 def setUp(self): |
375 AUTest.setUp(self) | 398 AUTest.setUp(self) |
376 | 399 |
| 400 @classmethod |
| 401 def ProcessOptions(cls, parser, options): |
| 402 """Processes non-vm-specific options.""" |
| 403 AUTest.ProcessOptions(parser, options) |
| 404 cls.remote = options.remote |
| 405 |
| 406 if not cls.remote: |
| 407 parser.error('We require a remote address for real tests.') |
| 408 |
377 def PrepareBase(self, image_path): | 409 def PrepareBase(self, image_path): |
378 """Auto-update to base image to prepare for test.""" | 410 """Auto-update to base image to prepare for test.""" |
379 self._UpdateImageReportError(image_path) | 411 self.PerformUpdate(image_path) |
380 | 412 |
381 def UpdateImage(self, image_path, stateful_change='old', proxy_port=None): | 413 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', |
| 414 proxy_port=None): |
382 """Updates a remote image using image_to_live.sh.""" | 415 """Updates a remote image using image_to_live.sh.""" |
383 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 416 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
384 cmd = ['%s/image_to_live.sh' % self.crosutils, | 417 cmd = ['%s/image_to_live.sh' % self.crosutils, |
385 '--image=%s' % image_path, | 418 '--image=%s' % image_path, |
386 '--remote=%s' % remote, | 419 '--remote=%s' % self.remote, |
387 stateful_change_flag, | 420 stateful_change_flag, |
388 '--verify', | 421 '--verify', |
389 '--src_image=%s' % self.source_image | 422 '--src_image=%s' % src_image_path |
390 ] | 423 ] |
391 | 424 |
392 if proxy_port: | 425 if proxy_port: |
393 cmd.append('--proxy_port=%s' % proxy_port) | 426 cmd.append('--proxy_port=%s' % proxy_port) |
394 | 427 |
395 if self.verbose: | 428 if self.verbose: |
396 try: | 429 try: |
397 RunCommand(cmd) | 430 RunCommand(cmd) |
398 except Exception, e: | 431 except Exception, e: |
399 raise UpdateException(1, e.message) | 432 raise UpdateException(1, e.message) |
400 else: | 433 else: |
401 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) | 434 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) |
402 if code != 0: | 435 if code != 0: |
403 raise UpdateException(code, stdout) | 436 raise UpdateException(code, stdout) |
404 | 437 |
405 def UpdateUsingPayload(self, | 438 def _UpdateUsingPayload(self, update_path, stateful_change='old', |
406 update_path, | |
407 stateful_change='old', | |
408 proxy_port=None): | 439 proxy_port=None): |
409 """Updates a remote image using image_to_live.sh.""" | 440 """Updates a remote image using image_to_live.sh.""" |
410 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 441 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
411 cmd = ['%s/image_to_live.sh' % self.crosutils, | 442 cmd = ['%s/image_to_live.sh' % self.crosutils, |
412 '--payload=%s' % update_path, | 443 '--payload=%s' % update_path, |
413 '--remote=%s' % remote, | 444 '--remote=%s' % self.remote, |
414 stateful_change_flag, | 445 stateful_change_flag, |
415 '--verify', | 446 '--verify', |
416 ] | 447 ] |
417 | 448 |
418 if proxy_port: | 449 if proxy_port: |
419 cmd.append('--proxy_port=%s' % proxy_port) | 450 cmd.append('--proxy_port=%s' % proxy_port) |
420 | 451 |
421 if self.verbose: | 452 if self.verbose: |
422 try: | 453 try: |
423 RunCommand(cmd) | 454 RunCommand(cmd) |
424 except Exception, e: | 455 except Exception, e: |
425 raise UpdateException(1, e.message) | 456 raise UpdateException(1, e.message) |
426 else: | 457 else: |
427 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) | 458 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) |
428 if code != 0: | 459 if code != 0: |
429 raise UpdateException(code, stdout) | 460 raise UpdateException(code, stdout) |
430 | 461 |
431 def VerifyImage(self, percent_required_to_pass): | 462 def VerifyImage(self, percent_required_to_pass): |
432 """Verifies an image using run_remote_tests.sh with verification suite.""" | 463 """Verifies an image using run_remote_tests.sh with verification suite.""" |
433 output = RunCommand([ | 464 output = RunCommand([ |
434 '%s/run_remote_tests.sh' % self.crosutils, | 465 '%s/run_remote_tests.sh' % self.crosutils, |
435 '--remote=%s' % remote, | 466 '--remote=%s' % self.remote, |
436 _VERIFY_SUITE, | 467 self.verify_suite, |
437 ], error_ok=True, enter_chroot=False, redirect_stdout=True) | 468 ], error_ok=True, enter_chroot=False, redirect_stdout=True) |
438 return self.CommonVerifyImage(self, output, percent_required_to_pass) | 469 return self.AssertEnoughTestsPassed(self, output, percent_required_to_pass) |
439 | 470 |
440 | 471 |
441 class VirtualAUTest(unittest.TestCase, AUTest): | 472 class VirtualAUTest(unittest.TestCase, AUTest): |
442 """Test harness for updating virtual machines.""" | 473 """Test harness for updating virtual machines.""" |
443 vm_image_path = None | 474 vm_image_path = None |
444 | 475 |
| 476 # VM Constants. |
| 477 _FULL_VDISK_SIZE = 6072 |
| 478 _FULL_STATEFULFS_SIZE = 3074 |
| 479 _KVM_PID_FILE = '/tmp/harness_pid' |
| 480 |
445 def _KillExistingVM(self, pid_file): | 481 def _KillExistingVM(self, pid_file): |
446 if os.path.exists(pid_file): | 482 if os.path.exists(pid_file): |
447 Warning('Existing %s found. Deleting and killing process' % | 483 Warning('Existing %s found. Deleting and killing process' % |
448 pid_file) | 484 pid_file) |
449 RunCommand(['./cros_stop_vm', '--kvm_pid=%s' % pid_file], | 485 RunCommand(['./cros_stop_vm', '--kvm_pid=%s' % pid_file], |
450 cwd=self.crosutilsbin) | 486 cwd=self.crosutilsbin) |
451 | 487 |
452 assert not os.path.exists(pid_file) | 488 assert not os.path.exists(pid_file) |
453 | 489 |
454 def setUp(self): | 490 def setUp(self): |
455 """Unit test overriden method. Is called before every test.""" | 491 """Unit test overriden method. Is called before every test.""" |
456 AUTest.setUp(self) | 492 AUTest.setUp(self) |
457 self._KillExistingVM(_KVM_PID_FILE) | 493 self._KillExistingVM(self._KVM_PID_FILE) |
| 494 |
| 495 @classmethod |
| 496 def ProcessOptions(cls, parser, options): |
| 497 """Processes vm-specific options.""" |
| 498 AUTest.ProcessOptions(parser, options) |
| 499 cls.board = options.board |
| 500 |
| 501 # Communicate flags to tests. |
| 502 cls.graphics_flag = '' |
| 503 if options.no_graphics: cls.graphics_flag = '--no_graphics' |
| 504 |
| 505 if not cls.board: |
| 506 parser.error('Need board to convert base image to vm.') |
458 | 507 |
459 def PrepareBase(self, image_path): | 508 def PrepareBase(self, image_path): |
460 """Creates an update-able VM based on base image.""" | 509 """Creates an update-able VM based on base image.""" |
461 self.vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( | 510 self.vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( |
462 image_path) | 511 image_path) |
463 | 512 |
464 Info('Creating: %s' % self.vm_image_path) | 513 Info('Creating: %s' % self.vm_image_path) |
465 | 514 |
466 if not os.path.exists(self.vm_image_path): | 515 if not os.path.exists(self.vm_image_path): |
467 Info('Qemu image %s not found, creating one.' % self.vm_image_path) | 516 Info('Qemu image %s not found, creating one.' % self.vm_image_path) |
468 RunCommand(['%s/image_to_vm.sh' % self.crosutils, | 517 RunCommand(['%s/image_to_vm.sh' % self.crosutils, |
469 '--full', | 518 '--full', |
470 '--from=%s' % ReinterpretPathForChroot( | 519 '--from=%s' % ReinterpretPathForChroot( |
471 os.path.dirname(image_path)), | 520 os.path.dirname(image_path)), |
472 '--vdisk_size=%s' % _FULL_VDISK_SIZE, | 521 '--vdisk_size=%s' % self._FULL_VDISK_SIZE, |
473 '--statefulfs_size=%s' % _FULL_STATEFULFS_SIZE, | 522 '--statefulfs_size=%s' % self._FULL_STATEFULFS_SIZE, |
474 '--board=%s' % board, | 523 '--board=%s' % self.board, |
475 '--test_image'], enter_chroot=True) | 524 '--test_image'], enter_chroot=True) |
476 else: | 525 else: |
477 Info('Using existing VM image %s' % self.vm_image_path) | 526 Info('Using existing VM image %s' % self.vm_image_path) |
478 | 527 |
479 | |
480 Info('Testing for %s' % self.vm_image_path) | 528 Info('Testing for %s' % self.vm_image_path) |
481 | |
482 self.assertTrue(os.path.exists(self.vm_image_path)) | 529 self.assertTrue(os.path.exists(self.vm_image_path)) |
483 | 530 |
484 def UpdateImage(self, image_path, stateful_change='old', proxy_port=None): | 531 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', |
| 532 proxy_port=None): |
485 """Updates VM image with image_path.""" | 533 """Updates VM image with image_path.""" |
486 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 534 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
487 if self.source_image == base_image_path: | 535 if src_image_path == self.base_image_path: |
488 self.source_image = self.vm_image_path | 536 src_image_path = self.vm_image_path |
489 | 537 |
490 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, | 538 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, |
491 '--update_image_path=%s' % image_path, | 539 '--update_image_path=%s' % image_path, |
492 '--vm_image_path=%s' % self.vm_image_path, | 540 '--vm_image_path=%s' % self.vm_image_path, |
493 '--snapshot', | 541 '--snapshot', |
494 vm_graphics_flag, | 542 self.graphics_flag, |
495 '--persist', | 543 '--persist', |
496 '--kvm_pid=%s' % _KVM_PID_FILE, | 544 '--kvm_pid=%s' % self._KVM_PID_FILE, |
497 stateful_change_flag, | 545 stateful_change_flag, |
498 '--src_image=%s' % self.source_image, | 546 '--src_image=%s' % src_image_path, |
499 ] | 547 ] |
500 | 548 |
501 if proxy_port: | 549 if proxy_port: |
502 cmd.append('--proxy_port=%s' % proxy_port) | 550 cmd.append('--proxy_port=%s' % proxy_port) |
503 | 551 |
504 if self.verbose: | 552 if self.verbose: |
505 try: | 553 try: |
506 RunCommand(cmd) | 554 RunCommand(cmd) |
507 except Exception, e: | 555 except Exception, e: |
508 raise UpdateException(1, e.message) | 556 raise UpdateException(1, e.message) |
509 else: | 557 else: |
510 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) | 558 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) |
511 if code != 0: | 559 if code != 0: |
512 raise UpdateException(code, stdout) | 560 raise UpdateException(code, stdout) |
513 | 561 |
514 def UpdateUsingPayload(self, | 562 def _UpdateUsingPayload(self, update_path, stateful_change='old', |
515 update_path, | |
516 stateful_change='old', | |
517 proxy_port=None): | 563 proxy_port=None): |
518 """Updates a remote image using image_to_live.sh.""" | 564 """Updates a vm image using cros_run_vm_update.""" |
519 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 565 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
520 if self.source_image == base_image_path: | |
521 self.source_image = self.vm_image_path | |
522 | |
523 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, | 566 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, |
524 '--payload=%s' % update_path, | 567 '--payload=%s' % update_path, |
525 '--vm_image_path=%s' % self.vm_image_path, | 568 '--vm_image_path=%s' % self.vm_image_path, |
526 '--snapshot', | 569 '--snapshot', |
527 vm_graphics_flag, | 570 self.graphics_flag, |
528 '--persist', | 571 '--persist', |
529 '--kvm_pid=%s' % _KVM_PID_FILE, | 572 '--kvm_pid=%s' % self._KVM_PID_FILE, |
530 stateful_change_flag, | 573 stateful_change_flag, |
531 '--src_image=%s' % self.source_image, | |
532 ] | 574 ] |
533 | 575 |
534 if proxy_port: | 576 if proxy_port: |
535 cmd.append('--proxy_port=%s' % proxy_port) | 577 cmd.append('--proxy_port=%s' % proxy_port) |
536 | 578 |
537 if self.verbose: | 579 if self.verbose: |
538 try: | 580 try: |
539 RunCommand(cmd) | 581 RunCommand(cmd) |
540 except Exception, e: | 582 except Exception, e: |
541 raise UpdateException(1, e.message) | 583 raise UpdateException(1, e.message) |
542 else: | 584 else: |
543 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) | 585 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) |
544 if code != 0: | 586 if code != 0: |
545 raise UpdateException(code, stdout) | 587 raise UpdateException(code, stdout) |
546 | 588 |
547 def VerifyImage(self, percent_required_to_pass): | 589 def VerifyImage(self, percent_required_to_pass): |
548 """Runs vm smoke suite to verify image.""" | 590 """Runs vm smoke suite to verify image.""" |
549 # image_to_live already verifies lsb-release matching. This is just | 591 # image_to_live already verifies lsb-release matching. This is just |
550 # for additional steps. | 592 # for additional steps. |
551 | 593 |
552 commandWithArgs = ['%s/cros_run_vm_test' % self.crosutilsbin, | 594 commandWithArgs = ['%s/cros_run_vm_test' % self.crosutilsbin, |
553 '--image_path=%s' % self.vm_image_path, | 595 '--image_path=%s' % self.vm_image_path, |
554 '--snapshot', | 596 '--snapshot', |
555 '--persist', | 597 '--persist', |
556 '--kvm_pid=%s' % _KVM_PID_FILE, | 598 '--kvm_pid=%s' % self._KVM_PID_FILE, |
557 _VERIFY_SUITE, | 599 self.verify_suite, |
558 ] | 600 ] |
559 | 601 |
560 if vm_graphics_flag: | 602 if self.graphics_flag: |
561 commandWithArgs.append(vm_graphics_flag) | 603 commandWithArgs.append(self.graphics_flag) |
562 | 604 |
563 output = RunCommand(commandWithArgs, error_ok=True, enter_chroot=False, | 605 output = RunCommand(commandWithArgs, error_ok=True, enter_chroot=False, |
564 redirect_stdout=True) | 606 redirect_stdout=True) |
565 return self.CommonVerifyImage(self, output, percent_required_to_pass) | 607 return self.AssertEnoughTestsPassed(self, output, percent_required_to_pass) |
566 | 608 |
567 | 609 |
568 if __name__ == '__main__': | 610 def main(): |
569 parser = optparse.OptionParser() | 611 parser = optparse.OptionParser() |
570 parser.add_option('-b', '--base_image', | 612 parser.add_option('-b', '--base_image', |
571 help='path to the base image.') | 613 help='path to the base image.') |
572 parser.add_option('-r', '--board', | 614 parser.add_option('-r', '--board', |
573 help='board for the images.') | 615 help='board for the images.') |
574 parser.add_option('--no_delta', action='store_false', default=True, | 616 parser.add_option('--no_delta', action='store_false', default=True, |
575 dest='delta', | 617 dest='delta', |
576 help='Disable using delta updates.') | 618 help='Disable using delta updates.') |
577 parser.add_option('--no_graphics', action='store_true', | 619 parser.add_option('--no_graphics', action='store_true', |
578 help='Disable graphics for the vm test.') | 620 help='Disable graphics for the vm test.') |
579 parser.add_option('-m', '--remote', | 621 parser.add_option('-m', '--remote', |
580 help='Remote address for real test.') | 622 help='Remote address for real test.') |
581 parser.add_option('-q', '--quick_test', default=False, action='store_true', | 623 parser.add_option('-q', '--quick_test', default=False, action='store_true', |
582 help='Use a basic test to verify image.') | 624 help='Use a basic test to verify image.') |
583 parser.add_option('-t', '--target_image', | 625 parser.add_option('-t', '--target_image', |
584 help='path to the target image.') | 626 help='path to the target image.') |
585 parser.add_option('--test_prefix', default='test', | 627 parser.add_option('--test_prefix', default='test', |
586 help='Only runs tests with specific prefix i.e. ' | 628 help='Only runs tests with specific prefix i.e. ' |
587 'testFullUpdateWipeStateful.') | 629 'testFullUpdateWipeStateful.') |
588 parser.add_option('-p', '--type', default='vm', | 630 parser.add_option('-p', '--type', default='vm', |
589 help='type of test to run: [vm, real]. Default: vm.') | 631 help='type of test to run: [vm, real]. Default: vm.') |
590 parser.add_option('--verbose', default=False, action='store_true', | 632 parser.add_option('--verbose', default=False, action='store_true', |
591 help='Print out rather than capture output as much as ' | 633 help='Print out rather than capture output as much as ' |
592 'possible.') | 634 'possible.') |
593 # Set the usage to include flags. | 635 (options, leftover_args) = parser.parse_args() |
594 parser.set_usage(parser.format_help()) | |
595 # Parse existing sys.argv so we can pass rest to unittest.main. | |
596 (options, sys.argv) = parser.parse_args(sys.argv) | |
597 | 636 |
598 AUTest.verbose = options.verbose | 637 if leftover_args: |
599 base_image_path = options.base_image | 638 parser.error('Found extra options we do not support: %s' % leftover_args) |
600 target_image_path = options.target_image | |
601 board = options.board | |
602 | |
603 if not base_image_path: | |
604 parser.error('Need path to base image for vm.') | |
605 elif not os.path.exists(base_image_path): | |
606 Die('%s does not exist' % base_image_path) | |
607 | |
608 if not target_image_path: | |
609 parser.error('Need path to target image to update with.') | |
610 elif not os.path.exists(target_image_path): | |
611 Die('%s does not exist' % target_image_path) | |
612 | |
613 if not board: | |
614 parser.error('Need board to convert base image to vm.') | |
615 | |
616 # Communicate flags to tests. | |
617 vm_graphics_flag = '' | |
618 if options.no_graphics: vm_graphics_flag = '--no_graphics' | |
619 if options.quick_test: _VERIFY_SUITE = 'build_RootFilesystemSize' | |
620 AUTest.use_delta_updates = options.delta | |
621 | |
622 # Only run the test harness we care about. | |
623 test_loader = unittest.TestLoader() | |
624 test_loader.testMethodPrefix = options.test_prefix | |
625 | 639 |
626 if options.type == 'vm': test_class = VirtualAUTest | 640 if options.type == 'vm': test_class = VirtualAUTest |
627 elif options.type == 'real': test_class = RealAUTest | 641 elif options.type == 'real': test_class = RealAUTest |
628 else: parser.error('Could not parse harness type %s.' % options.type) | 642 else: parser.error('Could not parse harness type %s.' % options.type) |
629 | 643 |
630 remote = options.remote | 644 test_class.ProcessOptions(parser, options) |
631 | 645 |
| 646 test_loader = unittest.TestLoader() |
| 647 test_loader.testMethodPrefix = options.test_prefix |
632 test_suite = test_loader.loadTestsFromTestCase(test_class) | 648 test_suite = test_loader.loadTestsFromTestCase(test_class) |
633 test_result = unittest.TextTestRunner(verbosity=2).run(test_suite) | 649 test_result = unittest.TextTestRunner(verbosity=2).run(test_suite) |
634 | 650 |
635 if not test_result.wasSuccessful(): | 651 if not test_result.wasSuccessful(): |
636 Die('Test harness was not successful') | 652 Die('Test harness was not successful') |
| 653 |
| 654 |
| 655 if __name__ == '__main__': |
| 656 main() |
OLD | NEW |