blob: a56a9eaed5a282aba5ae329d071f3ee4a1c9edb2 [file] [log] [blame]
Dan Talayco48370102010-03-03 15:17:33 -08001#!/usr/bin/env python
2"""
3@package oft
4
5OpenFlow test framework top level script
6
7This script is the entry point for running OpenFlow tests
8using the OFT framework.
9
10The global configuration is passed around in a dictionary
Brandon Heller88f709d2010-04-01 12:29:56 -070011generally called config. The keys have the following
Dan Talayco48370102010-03-03 15:17:33 -080012significance.
13
Dan Talayco2c0dba32010-03-06 22:47:06 -080014<pre>
Dan Talayco48370102010-03-03 15:17:33 -080015 platform : String identifying the target platform
16 controller_host : Host on which test controller is running (for sockets)
17 controller_port : Port on which test controller listens for switch cxn
18 port_count : (Optional) Number of ports in dataplane
19 base_of_port : (Optional) Base OpenFlow port number in dataplane
20 base_if_index : (Optional) Base OS network interface for dataplane
Dan Talayco2c0dba32010-03-06 22:47:06 -080021 test_dir : (TBD) Directory to search for test files (default .)
Dan Talayco48370102010-03-03 15:17:33 -080022 test_spec : (TBD) Specification of test(s) to run
23 log_file : Filename for test logging
Dan Talayco2c0dba32010-03-06 22:47:06 -080024 list : Boolean: List all tests and exit
Dan Talayco48370102010-03-03 15:17:33 -080025 debug : String giving debug level (info, warning, error...)
Dan Talayco2c0dba32010-03-06 22:47:06 -080026</pre>
Dan Talayco48370102010-03-03 15:17:33 -080027
28See config_defaults below for the default values.
29
Dan Talayco2c0dba32010-03-06 22:47:06 -080030The following are stored in the config dictionary, but are not currently
31configurable through the command line.
32
33<pre>
34 dbg_level : logging module value of debug level
35 port_map : Map of dataplane OpenFlow port to OS interface names
36 test_mod_map : Dictionary indexed by module names and whose value
37 is the module reference
38 all_tests : Dictionary indexed by module reference and whose
39 value is a list of functions in that module
40</pre>
41
Dan Talaycoc24aaae2010-07-08 14:05:24 -070042Each test may be assigned a priority by setting test_prio["TestName"] in
43the respective module. For now, the only use of this is to avoid
44automatic inclusion of tests into the default list. This is done by
45setting the test_prio value less than 0. Eventually we may add ordering
46of test execution by test priority.
47
Dan Talayco2c0dba32010-03-06 22:47:06 -080048To add a test to the system, either: edit an existing test case file (like
49basic.py) to add a test class which inherits from unittest.TestCase (directly
50or indirectly); or add a new file which includes a function definition
51test_set_init(config). Preferably the file is in the same directory as existing
52tests, though you can specify the directory on the command line. The file
53should not be called "all" as that's reserved for the test-spec.
54
55If you add a new file, the test_set_init function should record the port
56map object from the configuration along with whatever other configuration
57information it may need.
58
59TBD: To add configuration to the system, first add an entry to config_default
Dan Talayco48370102010-03-03 15:17:33 -080060below. If you want this to be a command line parameter, edit config_setup
61to add the option and default value to the parser. Then edit config_get
62to make sure the option value gets copied into the configuration
63structure (which then gets passed to everyone else).
64
65By convention, oft attempts to import the contents of a file by the
66name of $platform.py into the local namespace.
67
68IMPORTANT: That file should define a function platform_config_update which
69takes a configuration dictionary as an argument and updates it for the
70current run. In particular, it should set up config["port_map"] with
71the proper map from OF port numbers to OF interface names.
72
73You can add your own platform, say gp104, by adding a file gp104.py
74that defines the function platform_config_update and then use the
75parameter --platform=gp104 on the command line.
76
77If platform_config_update does not set config["port_map"], an attempt
78is made to generate a default map via the function default_port_map_setup.
79This will use "local" and "remote" for platform names if available
80and generate a sequential map based on the values of base_of_port and
81base_if_index in the configuration structure.
82
Dan Talayco48370102010-03-03 15:17:33 -080083The current model for test sets is basic.py. The current convention is
84that the test set should implement a function test_set_init which takes
85an oft configuration dictionary and returns a unittest.TestSuite object.
86Future test sets should do the same thing.
87
Dan Talayco52f64442010-03-03 15:32:41 -080088Default setup:
89
90The default setup runs locally using veth pairs. To exercise this,
91checkout and build an openflow userspace datapath. Then start it on
92the local host:
Dan Talayco2c0dba32010-03-06 22:47:06 -080093<pre>
Dan Talayco52f64442010-03-03 15:32:41 -080094 sudo ~/openflow/regress/bin/veth_setup.pl
95 sudo ofdatapath -i veth0,veth2,veth4,veth6 punix:/tmp/ofd &
96 sudo ofprotocol unix:/tmp/ofd tcp:127.0.0.1 --fail=closed --max-backoff=1 &
97
98Next, run oft:
99 sudo ./oft --debug=info
Dan Talayco2c0dba32010-03-06 22:47:06 -0800100</pre>
Dan Talayco52f64442010-03-03 15:32:41 -0800101
102Examine oft.log if things don't work.
Dan Talayco2c0dba32010-03-06 22:47:06 -0800103
Dan Talayco1a88c122010-03-07 22:00:20 -0800104@todo Support per-component debug levels (esp controller vs dataplane)
105@todo Consider moving oft up a level
Dan Talayco2c0dba32010-03-06 22:47:06 -0800106
Dan Talayco1a88c122010-03-07 22:00:20 -0800107Current test case setup:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800108 Files in this or sub directories (or later, directory specified on
109command line) that contain a function test_set_init are considered test
110files.
111 The function test_set_init examines the test_spec config variable
112and generates a suite of tests.
113 Support a command line option --test_mod so that all tests in that
114module will be run.
115 Support all to specify all tests from the module.
116
Dan Talayco48370102010-03-03 15:17:33 -0800117"""
118
119import sys
120from optparse import OptionParser
Dan Talayco2c0dba32010-03-06 22:47:06 -0800121from subprocess import Popen,PIPE
Dan Talayco48370102010-03-03 15:17:33 -0800122import logging
123import unittest
Dan Talayco2c0dba32010-03-06 22:47:06 -0800124import time
Brandon Heller446c1432010-04-01 12:43:27 -0700125import os
Dan Talayco48370102010-03-03 15:17:33 -0800126
Dan Talaycoba3745c2010-07-21 21:51:08 -0700127import testutils
128
Dan Talayco02eca0b2010-04-15 16:09:43 -0700129try:
130 import scapy.all as scapy
131except:
132 try:
133 import scapy as scapy
134 except:
135 sys.exit("Need to install scapy for packet parsing")
136
Dan Talayco48370102010-03-03 15:17:33 -0800137##@var DEBUG_LEVELS
138# Map from strings to debugging levels
139DEBUG_LEVELS = {
140 'debug' : logging.DEBUG,
141 'verbose' : logging.DEBUG,
142 'info' : logging.INFO,
143 'warning' : logging.WARNING,
144 'warn' : logging.WARNING,
145 'error' : logging.ERROR,
146 'critical' : logging.CRITICAL
147}
148
149_debug_default = "warning"
150_debug_level_default = DEBUG_LEVELS[_debug_default]
151
152##@var config_default
153# The default configuration dictionary for OFT
154config_default = {
Dan Talayco551befa2010-07-15 17:05:32 -0700155 "param" : None,
Dan Talayco48370102010-03-03 15:17:33 -0800156 "platform" : "local",
157 "controller_host" : "127.0.0.1",
158 "controller_port" : 6633,
159 "port_count" : 4,
160 "base_of_port" : 1,
161 "base_if_index" : 1,
Dan Talaycocf26b7a2011-08-05 10:15:35 -0700162 "relax" : False,
Dan Talayco2c0dba32010-03-06 22:47:06 -0800163 "test_spec" : "all",
164 "test_dir" : ".",
Dan Talayco48370102010-03-03 15:17:33 -0800165 "log_file" : "oft.log",
Dan Talayco2c0dba32010-03-06 22:47:06 -0800166 "list" : False,
Dan Talayco48370102010-03-03 15:17:33 -0800167 "debug" : _debug_default,
168 "dbg_level" : _debug_level_default,
Dan Talaycoac25cf32010-07-20 14:08:28 -0700169 "port_map" : {},
170 "test_params" : "None"
Dan Talayco48370102010-03-03 15:17:33 -0800171}
172
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700173# Default test priority
174TEST_PRIO_DEFAULT=100
175
Dan Talayco1a88c122010-03-07 22:00:20 -0800176#@todo Set up a dict of config params so easier to manage:
177# <param> <cmdline flags> <default value> <help> <optional parser>
178
Dan Talayco48370102010-03-03 15:17:33 -0800179# Map options to config structure
180def config_get(opts):
181 "Convert options class to OFT configuration dictionary"
182 cfg = config_default.copy()
Dan Talayco2c0dba32010-03-06 22:47:06 -0800183 for key in cfg.keys():
184 cfg[key] = eval("opts." + key)
185
186 # Special case checks
Dan Talayco48370102010-03-03 15:17:33 -0800187 if opts.debug not in DEBUG_LEVELS.keys():
188 print "Warning: Bad value specified for debug level; using default"
189 opts.debug = _debug_default
Dan Talayco02eca0b2010-04-15 16:09:43 -0700190 if opts.verbose:
191 cfg["debug"] = "verbose"
Dan Talayco48370102010-03-03 15:17:33 -0800192 cfg["dbg_level"] = DEBUG_LEVELS[cfg["debug"]]
Dan Talayco2c0dba32010-03-06 22:47:06 -0800193
Dan Talayco48370102010-03-03 15:17:33 -0800194 return cfg
195
196def config_setup(cfg_dflt):
197 """
198 Set up the configuration including parsing the arguments
199
200 @param cfg_dflt The default configuration dictionary
201 @return A pair (config, args) where config is an config
202 object and args is any additional arguments from the command line
203 """
204
205 parser = OptionParser(version="%prog 0.1")
206
Dan Talayco2c0dba32010-03-06 22:47:06 -0800207 #@todo parse port map as option?
Dan Talayco48370102010-03-03 15:17:33 -0800208 # Set up default values
Dan Talayco2c0dba32010-03-06 22:47:06 -0800209 for key in cfg_dflt.keys():
210 eval("parser.set_defaults("+key+"=cfg_dflt['"+key+"'])")
Dan Talayco48370102010-03-03 15:17:33 -0800211
Dan Talayco2c0dba32010-03-06 22:47:06 -0800212 #@todo Add options via dictionary
Dan Talayco48370102010-03-03 15:17:33 -0800213 plat_help = """Set the platform type. Valid values include:
214 local: User space virtual ethernet pair setup
215 remote: Remote embedded Broadcom based switch
Dan Talayco673e0852010-03-06 23:09:23 -0800216 Create a new_plat.py file and use --platform=new_plat on the command line
Dan Talayco48370102010-03-03 15:17:33 -0800217 """
218 parser.add_option("-P", "--platform", help=plat_help)
219 parser.add_option("-H", "--host", dest="controller_host",
220 help="The IP/name of the test controller host")
221 parser.add_option("-p", "--port", dest="controller_port",
222 type="int", help="Port number of the test controller")
Dan Talayco673e0852010-03-06 23:09:23 -0800223 test_list_help = """Indicate tests to run. Valid entries are "all" (the
224 default) or a comma separated list of:
225 module Run all tests in the named module
226 testcase Run tests in all modules with the name testcase
227 module.testcase Run the specific test case
228 """
229 parser.add_option("--test-spec", "--test-list", help=test_list_help)
Dan Talayco48370102010-03-03 15:17:33 -0800230 parser.add_option("--log-file",
231 help="Name of log file, empty string to log to console")
232 parser.add_option("--debug",
233 help="Debug lvl: debug, info, warning, error, critical")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700234 parser.add_option("--port-count", type="int",
Dan Talayco48370102010-03-03 15:17:33 -0800235 help="Number of ports to use (optional)")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700236 parser.add_option("--base-of-port", type="int",
Dan Talayco48370102010-03-03 15:17:33 -0800237 help="Base OpenFlow port number (optional)")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700238 parser.add_option("--base-if-index", type="int",
Dan Talayco2c0dba32010-03-06 22:47:06 -0800239 help="Base interface index number (optional)")
240 parser.add_option("--list", action="store_true",
Brandon Heller824504e2010-04-01 12:21:37 -0700241 help="List all tests and exit")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700242 parser.add_option("--verbose", action="store_true",
243 help="Short cut for --debug=verbose")
Dan Talaycocf26b7a2011-08-05 10:15:35 -0700244 parser.add_option("--relax", action="store_true",
245 help="Relax packet match checks allowing other packets")
Dan Talayco551befa2010-07-15 17:05:32 -0700246 parser.add_option("--param", type="int",
247 help="Parameter sent to test (for debugging)")
Dan Talaycoac25cf32010-07-20 14:08:28 -0700248 parser.add_option("-t", "--test-params",
249 help="Set test parameters: key=val;... See --list")
Dan Talayco48370102010-03-03 15:17:33 -0800250 # Might need this if other parsers want command line
251 # parser.allow_interspersed_args = False
252 (options, args) = parser.parse_args()
253
254 config = config_get(options)
255
256 return (config, args)
257
258def logging_setup(config):
259 """
260 Set up logging based on config
261 """
262 _format = "%(asctime)s %(name)-10s: %(levelname)-8s: %(message)s"
263 _datefmt = "%H:%M:%S"
Dan Talayco88fc8802010-03-07 11:37:52 -0800264 logging.basicConfig(filename=config["log_file"],
265 level=config["dbg_level"],
266 format=_format, datefmt=_datefmt)
Dan Talayco48370102010-03-03 15:17:33 -0800267
268def default_port_map_setup(config):
269 """
270 Setup the OF port mapping based on config
271 @param config The OFT configuration structure
272 @return Port map dictionary
273 """
274 if (config["base_of_port"] is None) or not config["port_count"]:
275 return None
276 port_map = {}
277 if config["platform"] == "local":
278 # For local, use every other veth port
279 for idx in range(config["port_count"]):
280 port_map[config["base_of_port"] + idx] = "veth" + \
281 str(config["base_if_index"] + (2 * idx))
282 elif config["platform"] == "remote":
283 # For remote, use eth ports
284 for idx in range(config["port_count"]):
285 port_map[config["base_of_port"] + idx] = "eth" + \
286 str(config["base_if_index"] + idx)
287 else:
288 return None
289
290 logging.info("Built default port map")
291 return port_map
292
Dan Talayco2c0dba32010-03-06 22:47:06 -0800293def test_list_generate(config):
294 """Generate the list of all known tests indexed by module name
295
296 Conventions: Test files must implement the function test_set_init
297
Dan Talayco1a88c122010-03-07 22:00:20 -0800298 Test cases are classes that implement runTest
Dan Talayco2c0dba32010-03-06 22:47:06 -0800299
300 @param config The oft configuration dictionary
301 @returns An array of triples (mod-name, module, [tests]) where
302 mod-name is the string (filename) of the module, module is the
303 value returned from __import__'ing the module and [tests] is an
304 array of strings giving the test cases from the module.
305 """
306
307 # Find and import test files
308 p1 = Popen(["find", config["test_dir"], "-type","f"], stdout = PIPE)
309 p2 = Popen(["xargs", "grep", "-l", "-e", "^def test_set_init"],
310 stdin=p1.stdout, stdout=PIPE)
311
312 all_tests = {}
313 mod_name_map = {}
314 # There's an extra empty entry at the end of the list
315 filelist = p2.communicate()[0].split("\n")[:-1]
316 for file in filelist:
Dan Talaycoac25cf32010-07-20 14:08:28 -0700317 if file[-1:] == '~' or file[0] == '#':
Dan Talaycode2a6392010-03-10 13:56:51 -0800318 continue
Dan Talayco2c0dba32010-03-06 22:47:06 -0800319 modfile = file.lstrip('./')[:-3]
320
321 try:
322 mod = __import__(modfile)
323 except:
324 logging.warning("Could not import file " + file)
325 continue
326 mod_name_map[modfile] = mod
327 added_fn = False
328 for fn in dir(mod):
329 if 'runTest' in dir(eval("mod." + fn)):
330 if not added_fn:
331 mod_name_map[modfile] = mod
332 all_tests[mod] = []
333 added_fn = True
334 all_tests[mod].append(fn)
335 config["all_tests"] = all_tests
336 config["mod_name_map"] = mod_name_map
337
338def die(msg, exit_val=1):
339 print msg
340 logging.critical(msg)
341 sys.exit(exit_val)
342
343def add_test(suite, mod, name):
344 logging.info("Adding test " + mod.__name__ + "." + name)
345 suite.addTest(eval("mod." + name)())
346
Dan Talayco79f36082010-03-11 16:53:53 -0800347def _space_to(n, str):
348 """
349 Generate a string of spaces to achieve width n given string str
350 If length of str >= n, return one space
351 """
352 spaces = n - len(str)
353 if spaces > 0:
354 return " " * spaces
355 return " "
356
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700357def test_prio_get(mod, test):
358 """
359 Return the priority of a test
360 If set in the test_prio variable for the module, return
361 that value. Otherwise return 100 (default)
362 """
363 if 'test_prio' in dir(mod):
364 if test in mod.test_prio.keys():
365 return mod.test_prio[test]
366 return TEST_PRIO_DEFAULT
367
Dan Talayco48370102010-03-03 15:17:33 -0800368#
369# Main script
370#
371
372# Get configuration, set up logging, import platform from file
373(config, args) = config_setup(config_default)
Dan Talayco48370102010-03-03 15:17:33 -0800374
Dan Talayco2c0dba32010-03-06 22:47:06 -0800375test_list_generate(config)
Dan Talaycocf26b7a2011-08-05 10:15:35 -0700376oft_config = config
Dan Talayco2c0dba32010-03-06 22:47:06 -0800377
378# Check if test list is requested; display and exit if so
379if config["list"]:
Dan Talayco79f36082010-03-11 16:53:53 -0800380 did_print = False
Dan Talayco2c0dba32010-03-06 22:47:06 -0800381 print "\nTest List:"
382 for mod in config["all_tests"].keys():
Dan Talayco79f36082010-03-11 16:53:53 -0800383 if config["test_spec"] != "all" and \
384 config["test_spec"] != mod.__name__:
385 continue
386 did_print = True
387 desc = mod.__doc__.strip()
388 desc = desc.split('\n')[0]
389 start_str = " Module " + mod.__name__ + ": "
390 print start_str + _space_to(22, start_str) + desc
Dan Talayco2c0dba32010-03-06 22:47:06 -0800391 for test in config["all_tests"][mod]:
Dan Talayco551befa2010-07-15 17:05:32 -0700392 try:
393 desc = eval('mod.' + test + '.__doc__.strip()')
394 desc = desc.split('\n')[0]
395 except:
396 desc = "No description"
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700397 if test_prio_get(mod, test) < 0:
398 start_str = " * " + test + ":"
399 else:
400 start_str = " " + test + ":"
Dan Talayco551befa2010-07-15 17:05:32 -0700401 if len(start_str) > 22:
402 desc = "\n" + _space_to(22, "") + desc
Dan Talayco79f36082010-03-11 16:53:53 -0800403 print start_str + _space_to(22, start_str) + desc
404 print
405 if not did_print:
406 print "No tests found for " + config["test_spec"]
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700407 else:
Dan Talayco7aa0b812010-07-20 14:51:41 -0700408 print "Tests preceded by * are not run by default"
409 print "Tests marked (TP1) after name take --test-params including:"
Dan Talaycoac25cf32010-07-20 14:08:28 -0700410 print " 'vid=N;strip_vlan=bool;add_vlan=bool'"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800411 sys.exit(0)
412
413logging_setup(config)
414logging.info("++++++++ " + time.asctime() + " ++++++++")
415
416# Generate the test suite
417#@todo Decide if multiple suites are ever needed
418suite = unittest.TestSuite()
419
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700420#@todo Allow specification of priority to override prio check
Dan Talayco2c0dba32010-03-06 22:47:06 -0800421if config["test_spec"] == "all":
422 for mod in config["all_tests"].keys():
423 for test in config["all_tests"][mod]:
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700424 # For now, a way to avoid tests
425 if test_prio_get(mod, test) >= 0:
426 add_test(suite, mod, test)
Dan Talayco2c0dba32010-03-06 22:47:06 -0800427
428else:
429 for ts_entry in config["test_spec"].split(","):
430 parts = ts_entry.split(".")
431
432 if len(parts) == 1: # Either a module or test name
433 if ts_entry in config["mod_name_map"].keys():
434 mod = config["mod_name_map"][ts_entry]
435 for test in config["all_tests"][mod]:
436 add_test(suite, mod, test)
437 else: # Search for matching tests
438 test_found = False
439 for mod in config["all_tests"].keys():
440 if ts_entry in config["all_tests"][mod]:
441 add_test(suite, mod, ts_entry)
442 test_found = True
443 if not test_found:
444 die("Could not find module or test: " + ts_entry)
445
446 elif len(parts) == 2: # module.test
447 if parts[0] not in config["mod_name_map"]:
448 die("Unknown module in test spec: " + ts_entry)
449 mod = config["mod_name_map"][parts[0]]
450 if parts[1] in config["all_tests"][mod]:
451 add_test(suite, mod, parts[1])
452 else:
453 die("No known test matches: " + ts_entry)
454
455 else:
456 die("Bad test spec: " + ts_entry)
457
458# Check if platform specified
Dan Talayco48370102010-03-03 15:17:33 -0800459if config["platform"]:
460 _imp_string = "from " + config["platform"] + " import *"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800461 logging.info("Importing platform: " + _imp_string)
Dan Talayco48370102010-03-03 15:17:33 -0800462 try:
463 exec(_imp_string)
464 except:
465 logging.warn("Failed to import " + config["platform"] + " file")
466
467try:
468 platform_config_update(config)
469except:
470 logging.warn("Could not run platform host configuration")
471
472if not config["port_map"]:
473 # Try to set up default port mapping if not done by platform
474 config["port_map"] = default_port_map_setup(config)
475
476if not config["port_map"]:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800477 die("Interface port map is not defined. Exiting")
Dan Talayco48370102010-03-03 15:17:33 -0800478
479logging.debug("Configuration: " + str(config))
480logging.info("OF port map: " + str(config["port_map"]))
481
482# Init the test sets
Dan Talayco2c0dba32010-03-06 22:47:06 -0800483for (modname,mod) in config["mod_name_map"].items():
484 try:
485 mod.test_set_init(config)
486 except:
487 logging.warning("Could not run test_set_init for " + modname)
Dan Talayco48370102010-03-03 15:17:33 -0800488
Dan Talayco2c0dba32010-03-06 22:47:06 -0800489if config["dbg_level"] == logging.CRITICAL:
490 _verb = 0
491elif config["dbg_level"] >= logging.WARNING:
492 _verb = 1
493else:
494 _verb = 2
Dan Talayco48370102010-03-03 15:17:33 -0800495
Brandon Heller446c1432010-04-01 12:43:27 -0700496if os.getuid() != 0:
497 print "ERROR: Super-user privileges required. Please re-run with " \
498 "sudo or as root."
499 exit(1)
500
Dan Talaycoac25cf32010-07-20 14:08:28 -0700501
502if __name__ == "__main__":
503 logging.info("*** TEST RUN START: " + time.asctime())
504 unittest.TextTestRunner(verbosity=_verb).run(suite)
Dan Talaycoba3745c2010-07-21 21:51:08 -0700505 if testutils.skipped_test_count > 0:
506 ts = " tests"
507 if testutils.skipped_test_count == 1: ts = " test"
508 logging.info("Skipped " + str(testutils.skipped_test_count) + ts)
509 print("Skipped " + str(testutils.skipped_test_count) + ts)
Dan Talaycoac25cf32010-07-20 14:08:28 -0700510 logging.info("*** TEST RUN END : " + time.asctime())
Dan Talaycoba3745c2010-07-21 21:51:08 -0700511
Dan Talayco48370102010-03-03 15:17:33 -0800512