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