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) 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 sys | 10 import sys |
10 import unittest | 11 import unittest |
12 import urllib | |
11 | 13 |
12 sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) | 14 sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) |
13 from cros_build_lib import Die | 15 from cros_build_lib import Die |
14 from cros_build_lib import Info | 16 from cros_build_lib import Info |
15 from cros_build_lib import ReinterpretPathForChroot | 17 from cros_build_lib import ReinterpretPathForChroot |
16 from cros_build_lib import RunCommand | 18 from cros_build_lib import RunCommand |
19 from cros_build_lib import RunCommandCaptureOutput | |
17 from cros_build_lib import Warning | 20 from cros_build_lib import Warning |
18 | 21 |
19 # VM Constants. | 22 # VM Constants. |
20 _FULL_VDISK_SIZE = 6072 | 23 _FULL_VDISK_SIZE = 6072 |
21 _FULL_STATEFULFS_SIZE = 3074 | 24 _FULL_STATEFULFS_SIZE = 3074 |
22 _KVM_PID_FILE = '/tmp/harness_pid' | 25 _KVM_PID_FILE = '/tmp/harness_pid' |
23 _VERIFY_SUITE = 'suite_Smoke' | 26 _VERIFY_SUITE = 'suite_Smoke' |
24 | 27 |
25 # Globals to communicate options to unit tests. | 28 # Globals to communicate options to unit tests. |
26 global base_image_path | 29 global base_image_path |
27 global board | 30 global board |
28 global remote | 31 global remote |
29 global target_image_path | 32 global target_image_path |
30 global vm_graphics_flag | 33 global vm_graphics_flag |
31 | 34 |
35 class UpdateException(Exception): | |
36 """Exception thrown when UpdateImage or UpdateUsingPayload fail""" | |
37 def __init__(self, code, stdout): | |
38 self.code = code | |
39 self.stdout = stdout | |
32 | 40 |
33 class AUTest(object): | 41 class AUTest(object): |
34 """Abstract interface that defines an Auto Update test.""" | 42 """Abstract interface that defines an Auto Update test.""" |
35 source_image = '' | 43 source_image = '' |
36 use_delta_updates = False | 44 use_delta_updates = False |
37 | 45 |
38 def setUp(self): | 46 def setUp(self): |
39 unittest.TestCase.setUp(self) | 47 unittest.TestCase.setUp(self) |
40 # Set these up as they are used often. | 48 # Set these up as they are used often. |
41 self.crosutils = os.path.join(os.path.dirname(__file__), '..') | 49 self.crosutils = os.path.join(os.path.dirname(__file__), '..') |
42 self.crosutilsbin = os.path.join(os.path.dirname(__file__)) | 50 self.crosutilsbin = os.path.join(os.path.dirname(__file__)) |
51 self.download_folder = os.path.join(self.crosutilsbin, 'latest_download') | |
43 | 52 |
44 def GetStatefulChangeFlag(self, stateful_change): | 53 def GetStatefulChangeFlag(self, stateful_change): |
45 """Returns the flag to pass to image_to_vm for the stateful change.""" | 54 """Returns the flag to pass to image_to_vm for the stateful change.""" |
46 stateful_change_flag = '' | 55 stateful_change_flag = '' |
47 if stateful_change: | 56 if stateful_change: |
48 stateful_change_flag = '--stateful_update_flag=%s' % stateful_change | 57 stateful_change_flag = '--stateful_update_flag=%s' % stateful_change |
49 | 58 |
50 return stateful_change_flag | 59 return stateful_change_flag |
51 | 60 |
52 def ParseGenerateTestReportOutput(self, output): | 61 def ParseGenerateTestReportOutput(self, output): |
(...skipping 14 matching lines...) Expand all Loading... | |
67 def TryDeltaAndFallbackToFull(self, src_image, image, stateful_change='old'): | 76 def TryDeltaAndFallbackToFull(self, src_image, image, stateful_change='old'): |
68 """Tries the delta update first if set and falls back to full update.""" | 77 """Tries the delta update first if set and falls back to full update.""" |
69 if self.use_delta_updates: | 78 if self.use_delta_updates: |
70 try: | 79 try: |
71 self.source_image = src_image | 80 self.source_image = src_image |
72 self.UpdateImage(image) | 81 self.UpdateImage(image) |
73 except: | 82 except: |
74 Warning('Delta update failed, disabling delta updates and retrying.') | 83 Warning('Delta update failed, disabling delta updates and retrying.') |
75 self.use_delta_updates = False | 84 self.use_delta_updates = False |
76 self.source_image = '' | 85 self.source_image = '' |
77 self.UpdateImage(image) | 86 self._UpdateImageReportError(image) |
78 else: | 87 else: |
79 self.UpdateImage(image) | 88 self._UpdateImageReportError(image) |
80 | 89 |
81 def PrepareBase(self): | 90 def _UpdateImageReportError(self, image_path, stateful_change='old'): |
91 """Calls UpdateImage and reports any error to the console. | |
92 | |
93 Still throws the exception. | |
94 """ | |
95 try: | |
96 self.UpdateImage(image_path, stateful_change) | |
97 except UpdateException as err: | |
98 # If the update fails, print it out | |
99 Warning(err.stdout) | |
100 raise | |
101 | |
102 def _AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): | |
103 # This update is expected to fail... | |
104 try: | |
105 self.UpdateUsingPayload(payload) | |
106 except UpdateException as err: | |
107 # Will raise ValueError if expected is not found. | |
108 if re.search(re.escape(expected_msg), err.stdout, re.MULTILINE): | |
109 return | |
110 | |
111 Warning("Didn't find '%s' in:" % expected_msg) | |
112 Warning(err.stdout) | |
113 self.fail('We managed to update when failure was expected') | |
114 | |
115 def PrepareBase(self, image_path): | |
82 """Prepares target with base_image_path.""" | 116 """Prepares target with base_image_path.""" |
83 pass | 117 pass |
84 | 118 |
85 def UpdateImage(self, image_path, stateful_change='old'): | 119 def UpdateImage(self, image_path, stateful_change='old'): |
86 """Updates target with the image given by the image_path. | 120 """Updates target with the image given by the image_path. |
87 | 121 |
88 Args: | 122 Args: |
89 image_path: Path to the image to update with. This image must be a test | 123 image_path: Path to the image to update with. This image must be a test |
90 image. | 124 image. |
91 stateful_change: How to modify the stateful partition. Values are: | 125 stateful_change: How to modify the stateful partition. Values are: |
92 'old': Don't modify stateful partition. Just update normally. | 126 'old': Don't modify stateful partition. Just update normally. |
93 'clean': Uses clobber-state to wipe the stateful partition with the | 127 'clean': Uses clobber-state to wipe the stateful partition with the |
94 exception of code needed for ssh. | 128 exception of code needed for ssh. |
95 """ | 129 """ |
96 pass | 130 pass |
97 | 131 |
132 def UpdateUsingPayload(self, update_path, stateful_change='old'): | |
133 """Updates target with the pre-generated update stored in update_path | |
134 | |
135 Args: | |
136 update_path: Path to the image to update with. This directory should | |
137 contain both update.gz, and stateful.image.gz | |
138 """ | |
139 pass | |
140 | |
98 def VerifyImage(self, percent_required_to_pass): | 141 def VerifyImage(self, percent_required_to_pass): |
99 """Verifies the image with tests. | 142 """Verifies the image with tests. |
100 | 143 |
101 Verifies that the test images passes the percent required. | 144 Verifies that the test images passes the percent required. |
102 | 145 |
103 Args: | 146 Args: |
104 percent_required_to_pass: percentage required to pass. This should be | 147 percent_required_to_pass: percentage required to pass. This should be |
105 fall between 0-100. | 148 fall between 0-100. |
106 | 149 |
107 Returns: | 150 Returns: |
(...skipping 25 matching lines...) Expand all Loading... | |
133 return percent_passed | 176 return percent_passed |
134 | 177 |
135 def testFullUpdateKeepStateful(self): | 178 def testFullUpdateKeepStateful(self): |
136 """Tests if we can update normally. | 179 """Tests if we can update normally. |
137 | 180 |
138 This test checks that we can update by updating the stateful partition | 181 This test checks that we can update by updating the stateful partition |
139 rather than wiping it. | 182 rather than wiping it. |
140 """ | 183 """ |
141 # Just make sure some tests pass on original image. Some old images | 184 # Just make sure some tests pass on original image. Some old images |
142 # don't pass many tests. | 185 # don't pass many tests. |
143 self.PrepareBase() | 186 self.PrepareBase(base_image_path) |
144 # TODO(sosa): move to 100% once we start testing using the autotest paired | 187 # TODO(sosa): move to 100% once we start testing using the autotest paired |
145 # with the dev channel. | 188 # with the dev channel. |
146 percent_passed = self.VerifyImage(10) | 189 percent_passed = self.VerifyImage(10) |
147 | 190 |
148 # Update to - all tests should pass on new image. | 191 # Update to - all tests should pass on new image. |
149 Info('Updating from base image on vm to target image.') | 192 Info('Updating from base image on vm to target image.') |
150 self.TryDeltaAndFallbackToFull(base_image_path, target_image_path) | 193 self.TryDeltaAndFallbackToFull(base_image_path, target_image_path) |
151 self.VerifyImage(100) | 194 self.VerifyImage(100) |
152 | 195 |
153 # Update from - same percentage should pass that originally passed. | 196 # Update from - same percentage should pass that originally passed. |
154 Info('Updating from updated image on vm back to base image.') | 197 Info('Updating from updated image on vm back to base image.') |
155 self.TryDeltaAndFallbackToFull(target_image_path, base_image_path) | 198 self.TryDeltaAndFallbackToFull(target_image_path, base_image_path) |
156 self.VerifyImage(percent_passed) | 199 self.VerifyImage(percent_passed) |
157 | 200 |
158 def testFullUpdateWipeStateful(self): | 201 def testFullUpdateWipeStateful(self): |
159 """Tests if we can update after cleaning the stateful partition. | 202 """Tests if we can update after cleaning the stateful partition. |
160 | 203 |
161 This test checks that we can update successfully after wiping the | 204 This test checks that we can update successfully after wiping the |
162 stateful partition. | 205 stateful partition. |
163 """ | 206 """ |
164 # Just make sure some tests pass on original image. Some old images | 207 # Just make sure some tests pass on original image. Some old images |
165 # don't pass many tests. | 208 # don't pass many tests. |
166 self.PrepareBase() | 209 self.PrepareBase(base_image_path) |
sosa
2010/12/03 01:07:38
use image_path?
| |
167 # TODO(sosa): move to 100% once we start testing using the autotest paired | 210 # TODO(sosa): move to 100% once we start testing using the autotest paired |
168 # with the dev channel. | 211 # with the dev channel. |
169 percent_passed = self.VerifyImage(10) | 212 percent_passed = self.VerifyImage(10) |
170 | 213 |
171 # Update to - all tests should pass on new image. | 214 # Update to - all tests should pass on new image. |
172 Info('Updating from base image on vm to target image and wiping stateful.') | 215 Info('Updating from base image on vm to target image and wiping stateful.') |
173 self.TryDeltaAndFallbackToFull(base_image_path, target_image_path, 'clean') | 216 self.TryDeltaAndFallbackToFull(base_image_path, target_image_path, 'clean') |
174 self.VerifyImage(100) | 217 self.VerifyImage(100) |
175 | 218 |
176 # Update from - same percentage should pass that originally passed. | 219 # Update from - same percentage should pass that originally passed. |
177 Info('Updating from updated image back to base image and wiping stateful.') | 220 Info('Updating from updated image back to base image and wiping stateful.') |
178 self.TryDeltaAndFallbackToFull(target_image_path, base_image_path, 'clean') | 221 self.TryDeltaAndFallbackToFull(target_image_path, base_image_path, 'clean') |
179 self.VerifyImage(percent_passed) | 222 self.VerifyImage(percent_passed) |
180 | 223 |
224 def testPartialUpdate(self): | |
225 """Tests what happens if we attempt to update with a truncated payload.""" | |
226 # Preload with the version we are trying to test. | |
227 self.PrepareBase(target_image_path) | |
228 | |
229 # Image can be updated at: | |
230 # ~chrome-eng/chromeos/localmirror/autest-images | |
231 url = 'http://gsdview.appspot.com/chromeos-localmirror/' \ | |
232 'autest-images/truncated_image.gz' | |
233 payload = os.path.join(self.download_folder, 'truncated_image.gz') | |
234 | |
235 # Read from the URL and write to the local file | |
236 urllib.urlretrieve(url, payload) | |
237 | |
238 expected_msg='download_hash_data == update_check_response_hash failed' | |
239 self._AttemptUpdateWithPayloadExpectedFailure(payload, expected_msg) | |
240 | |
241 def testCorruptedUpdate(self): | |
242 """Tests what happens if we attempt to update with a corrupted payload.""" | |
243 # Preload with the version we are trying to test. | |
244 self.PrepareBase(target_image_path) | |
245 | |
246 # Image can be updated at: | |
247 # ~chrome-eng/chromeos/localmirror/autest-images | |
248 url = 'http://gsdview.appspot.com/chromeos-localmirror/' \ | |
249 'autest-images/corrupted_image.gz' | |
250 payload = os.path.join(self.download_folder, 'corrupted.gz') | |
251 | |
252 # Read from the URL and write to the local file | |
253 urllib.urlretrieve(url, payload) | |
254 | |
255 # This update is expected to fail... | |
256 expected_msg='zlib inflate() error:-3' | |
257 self._AttemptUpdateWithPayloadExpectedFailure(payload, expected_msg) | |
181 | 258 |
182 class RealAUTest(unittest.TestCase, AUTest): | 259 class RealAUTest(unittest.TestCase, AUTest): |
183 """Test harness for updating real images.""" | 260 """Test harness for updating real images.""" |
184 | 261 |
185 def setUp(self): | 262 def setUp(self): |
186 AUTest.setUp(self) | 263 AUTest.setUp(self) |
187 | 264 |
188 def PrepareBase(self): | 265 def PrepareBase(self, image_path): |
189 """Auto-update to base image to prepare for test.""" | 266 """Auto-update to base image to prepare for test.""" |
190 self.UpdateImage(base_image_path) | 267 self._UpdateImageReportError(image_path) |
191 | 268 |
192 def UpdateImage(self, image_path, stateful_change='old'): | 269 def UpdateImage(self, image_path, stateful_change='old'): |
193 """Updates a remote image using image_to_live.sh.""" | 270 """Updates a remote image using image_to_live.sh.""" |
194 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 271 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
195 | 272 |
196 RunCommand([ | 273 (code, stdout, stderr) = RunCommandCaptureOutput([ |
197 '%s/image_to_live.sh' % self.crosutils, | 274 '%s/image_to_live.sh' % self.crosutils, |
198 '--image=%s' % image_path, | 275 '--image=%s' % image_path, |
199 '--remote=%s' % remote, | 276 '--remote=%s' % remote, |
200 stateful_change_flag, | 277 stateful_change_flag, |
201 '--verify', | 278 '--verify', |
202 '--src_image=%s' % self.source_image, | 279 '--src_image=%s' % self.source_image |
203 ], enter_chroot=False) | 280 ]) |
204 | 281 |
282 if code != 0: | |
283 raise UpdateException(code, stdout) | |
284 | |
285 def UpdateUsingPayload(self, update_path, stateful_change='old'): | |
286 """Updates a remote image using image_to_live.sh.""" | |
287 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | |
288 | |
289 (code, stdout, stderr) = RunCommandCaptureOutput([ | |
290 '%s/image_to_live.sh' % self.crosutils, | |
291 '--payload=%s' % update_path, | |
292 '--remote=%s' % remote, | |
293 stateful_change_flag, | |
294 '--verify', | |
295 ]) | |
296 | |
297 if code != 0: | |
298 raise UpdateException(code, stdout) | |
205 | 299 |
206 def VerifyImage(self, percent_required_to_pass): | 300 def VerifyImage(self, percent_required_to_pass): |
207 """Verifies an image using run_remote_tests.sh with verification suite.""" | 301 """Verifies an image using run_remote_tests.sh with verification suite.""" |
208 output = RunCommand([ | 302 output = RunCommand([ |
209 '%s/run_remote_tests.sh' % self.crosutils, | 303 '%s/run_remote_tests.sh' % self.crosutils, |
210 '--remote=%s' % remote, | 304 '--remote=%s' % remote, |
211 _VERIFY_SUITE, | 305 _VERIFY_SUITE, |
212 ], error_ok=True, enter_chroot=False, redirect_stdout=True) | 306 ], error_ok=True, enter_chroot=False, redirect_stdout=True) |
213 return self.CommonVerifyImage(self, output, percent_required_to_pass) | 307 return self.CommonVerifyImage(self, output, percent_required_to_pass) |
214 | 308 |
(...skipping 11 matching lines...) Expand all Loading... | |
226 if pid: | 320 if pid: |
227 RunCommand(['sudo', 'kill', pid.strip()], error_ok=True, | 321 RunCommand(['sudo', 'kill', pid.strip()], error_ok=True, |
228 enter_chroot=False) | 322 enter_chroot=False) |
229 RunCommand(['sudo', 'rm', pid_file], enter_chroot=False) | 323 RunCommand(['sudo', 'rm', pid_file], enter_chroot=False) |
230 | 324 |
231 def setUp(self): | 325 def setUp(self): |
232 """Unit test overriden method. Is called before every test.""" | 326 """Unit test overriden method. Is called before every test.""" |
233 AUTest.setUp(self) | 327 AUTest.setUp(self) |
234 self._KillExistingVM(_KVM_PID_FILE) | 328 self._KillExistingVM(_KVM_PID_FILE) |
235 | 329 |
236 def PrepareBase(self): | 330 def PrepareBase(self, image_path): |
237 """Creates an update-able VM based on base image.""" | 331 """Creates an update-able VM based on base image.""" |
238 self.vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( | 332 self.vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( |
239 base_image_path) | 333 image_path) |
334 | |
335 Info('Creating: %s' % self.vm_image_path) | |
240 | 336 |
241 if not os.path.exists(self.vm_image_path): | 337 if not os.path.exists(self.vm_image_path): |
242 Info('Qemu image %s not found, creating one.' % self.vm_image_path) | 338 Info('Qemu image %s not found, creating one.' % self.vm_image_path) |
243 RunCommand(['%s/image_to_vm.sh' % self.crosutils, | 339 RunCommand(['%s/image_to_vm.sh' % self.crosutils, |
244 '--full', | 340 '--full', |
245 '--from=%s' % ReinterpretPathForChroot( | 341 '--from=%s' % ReinterpretPathForChroot( |
246 os.path.dirname(base_image_path)), | 342 os.path.dirname(image_path)), |
247 '--vdisk_size=%s' % _FULL_VDISK_SIZE, | 343 '--vdisk_size=%s' % _FULL_VDISK_SIZE, |
248 '--statefulfs_size=%s' % _FULL_STATEFULFS_SIZE, | 344 '--statefulfs_size=%s' % _FULL_STATEFULFS_SIZE, |
249 '--board=%s' % board, | 345 '--board=%s' % board, |
250 '--test_image'], enter_chroot=True) | 346 '--test_image'], enter_chroot=True) |
251 else: | 347 else: |
252 Info('Using existing VM image %s' % self.vm_image_path) | 348 Info('Using existing VM image %s' % self.vm_image_path) |
253 | 349 |
350 | |
351 Info('Testing for %s' % self.vm_image_path) | |
352 | |
254 self.assertTrue(os.path.exists(self.vm_image_path)) | 353 self.assertTrue(os.path.exists(self.vm_image_path)) |
255 | 354 |
256 def UpdateImage(self, image_path, stateful_change='old'): | 355 def UpdateImage(self, image_path, stateful_change='old'): |
257 """Updates VM image with image_path.""" | 356 """Updates VM image with image_path.""" |
258 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 357 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
259 if self.source_image == base_image_path: | 358 if self.source_image == base_image_path: |
260 self.source_image = self.vm_image_path | 359 self.source_image = self.vm_image_path |
261 | 360 |
262 RunCommand(['%s/cros_run_vm_update' % self.crosutilsbin, | 361 (code, stdout, stderr) = RunCommandCaptureOutput([ |
263 '--update_image_path=%s' % image_path, | 362 '%s/cros_run_vm_update' % self.crosutilsbin, |
264 '--vm_image_path=%s' % self.vm_image_path, | 363 '--update_image_path=%s' % image_path, |
265 '--snapshot', | 364 '--vm_image_path=%s' % self.vm_image_path, |
266 vm_graphics_flag, | 365 '--snapshot', |
267 '--persist', | 366 vm_graphics_flag, |
268 '--kvm_pid=%s' % _KVM_PID_FILE, | 367 '--persist', |
269 stateful_change_flag, | 368 '--kvm_pid=%s' % _KVM_PID_FILE, |
270 '--src_image=%s' % self.source_image, | 369 stateful_change_flag, |
271 ], enter_chroot=False) | 370 '--src_image=%s' % self.source_image, |
371 ]) | |
372 | |
373 if code != 0: | |
374 raise UpdateException(code, stdout) | |
375 | |
376 def UpdateUsingPayload(self, update_path, stateful_change='old'): | |
377 """Updates a remote image using image_to_live.sh.""" | |
378 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | |
379 if self.source_image == base_image_path: | |
380 self.source_image = self.vm_image_path | |
381 | |
382 (code, stdout, stderr) = RunCommandCaptureOutput([ | |
383 '%s/cros_run_vm_update' % self.crosutilsbin, | |
384 '--payload=%s' % update_path, | |
385 '--vm_image_path=%s' % self.vm_image_path, | |
386 '--snapshot', | |
387 vm_graphics_flag, | |
388 '--persist', | |
389 '--kvm_pid=%s' % _KVM_PID_FILE, | |
390 stateful_change_flag, | |
391 '--src_image=%s' % self.source_image, | |
392 ]) | |
393 | |
394 if code != 0: | |
395 raise UpdateException(code, stdout) | |
272 | 396 |
273 def VerifyImage(self, percent_required_to_pass): | 397 def VerifyImage(self, percent_required_to_pass): |
274 """Runs vm smoke suite to verify image.""" | 398 """Runs vm smoke suite to verify image.""" |
275 # image_to_live already verifies lsb-release matching. This is just | 399 # image_to_live already verifies lsb-release matching. This is just |
276 # for additional steps. | 400 # for additional steps. |
277 | 401 |
278 commandWithArgs = ['%s/cros_run_vm_test' % self.crosutilsbin, | 402 commandWithArgs = ['%s/cros_run_vm_test' % self.crosutilsbin, |
279 '--image_path=%s' % self.vm_image_path, | 403 '--image_path=%s' % self.vm_image_path, |
280 '--snapshot', | 404 '--snapshot', |
281 '--persist', | 405 '--persist', |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
348 else: | 472 else: |
349 remote = options.remote | 473 remote = options.remote |
350 | 474 |
351 suite = unittest.TestLoader().loadTestsFromTestCase(RealAUTest) | 475 suite = unittest.TestLoader().loadTestsFromTestCase(RealAUTest) |
352 test_result = unittest.TextTestRunner(verbosity=2).run(suite) | 476 test_result = unittest.TextTestRunner(verbosity=2).run(suite) |
353 else: | 477 else: |
354 parser.error('Could not parse harness type %s.' % options.type) | 478 parser.error('Could not parse harness type %s.' % options.type) |
355 | 479 |
356 if not test_result.wasSuccessful(): | 480 if not test_result.wasSuccessful(): |
357 Die('Test harness was not successful') | 481 Die('Test harness was not successful') |
OLD | NEW |