3 """Automated tests of the apt-p2p functionality.
5 This script runs several automatic tests of some of the functionality in
8 @type tests: C{dictionary}
9 @var tests: all of the tests that can be run.
10 The keys are the test names (strings) which are used on the command-line
11 to identify the tests (can not be 'all' or 'help'). The values are tuples
12 with four elements: a description of the test (C{string}), the bootstrap
13 nodes to start (C{dictionary}), the downloaders to start (C{dictionary},
14 and the apt-get commands to run (C{list}).
16 The bootstrap nodes keys are integers, which must be in the range 1-9.
17 The values are the dictionary of keyword options to pass to the function
18 that starts the bootstrap node (see L{start_bootstrap} below).
20 The downloaders keys are also integers in the range 1-99. The values are
21 the dictionary of keyword options to pass to the function
22 that starts the downloader node (see L{start_downloader} below).
24 The apt-get commands' list elements are tuples with 2 elements: the
25 downloader to run the command on, and the list of command-line
26 arguments to specify to the apt-get program.
29 @var CWD: the working directory the script was run from
30 @type apt_conf_template: C{string}
31 @var apt_conf_template: the template to use for the apt.conf file
35 from time import sleep, time
36 import sys, os, signal
37 from traceback import print_exc
38 from os.path import exists
40 tests = {'1': ('Start a single bootstrap and downloader, test updating and downloading ' +
45 (1, ['install', 'aboot-base']),
46 (1, ['install', 'aap-doc']),
47 (1, ['install', 'ada-reference-manual']),
48 (1, ['install', 'aspectj-doc']),
49 (1, ['install', 'fop-doc']),
50 (1, ['install', 'jswat-doc']),
51 (1, ['install', 'asis-doc']),
52 (1, ['install', 'bison-doc']),
53 (1, ['install', 'crash-whitepaper']),
54 (1, ['install', 'doc-iana']),
57 '2': ('Start a single bootstrap and 2 downloaders to test downloading from a peer.',
63 (1, ['install', 'aboot-base']),
64 (2, ['install', 'aboot-base']),
65 (1, ['install', 'aap-doc']),
66 (1, ['install', 'ada-reference-manual']),
67 (1, ['install', 'fop-doc']),
68 (1, ['install', 'jswat-doc']),
69 (1, ['install', 'bison-doc']),
70 (1, ['install', 'crash-whitepaper']),
71 (2, ['install', 'aap-doc']),
72 (2, ['install', 'ada-reference-manual']),
73 (2, ['install', 'fop-doc']),
74 (2, ['install', 'jswat-doc']),
75 (2, ['install', 'bison-doc']),
76 (2, ['install', 'crash-whitepaper']),
79 '3': ('Start a single bootstrap and 6 downloaders, to test downloading' +
80 ' speeds from each other.',
91 (1, ['install', 'aboot-base']),
92 (1, ['install', 'ada-reference-manual']),
93 (1, ['install', 'fop-doc']),
94 (1, ['install', 'doc-iana']),
95 (2, ['install', 'aboot-base']),
96 (2, ['install', 'ada-reference-manual']),
97 (2, ['install', 'fop-doc']),
98 (2, ['install', 'doc-iana']),
99 (3, ['install', 'aboot-base']),
100 (3, ['install', 'ada-reference-manual']),
101 (3, ['install', 'fop-doc']),
102 (3, ['install', 'doc-iana']),
104 (4, ['install', 'aboot-base']),
105 (4, ['install', 'ada-reference-manual']),
106 (4, ['install', 'fop-doc']),
107 (4, ['install', 'doc-iana']),
109 (5, ['install', 'aboot-base']),
110 (5, ['install', 'ada-reference-manual']),
111 (5, ['install', 'fop-doc']),
112 (5, ['install', 'doc-iana']),
114 (6, ['install', 'aboot-base']),
115 (6, ['install', 'ada-reference-manual']),
116 (6, ['install', 'fop-doc']),
117 (6, ['install', 'doc-iana']),
120 '4': ('Start a single bootstrap and 1 downloader, requesting the same' +
121 ' packages multiple times to test caching.',
125 (1, ['install', 'aboot-base']),
126 (1, ['install', 'ada-reference-manual']),
127 (1, ['install', 'fop-doc']),
128 (1, ['install', 'doc-iana']),
130 (1, ['install', 'aboot-base']),
131 (1, ['install', 'ada-reference-manual']),
132 (1, ['install', 'fop-doc']),
133 (1, ['install', 'doc-iana']),
135 (1, ['install', 'aboot-base']),
136 (1, ['install', 'ada-reference-manual']),
137 (1, ['install', 'fop-doc']),
138 (1, ['install', 'doc-iana']),
141 '5': ('Start a single bootstrap and 6 downloaders, update all to test' +
142 ' that they can all see each other.',
144 {1: ([], {'suites': 'contrib non-free'}),
145 2: ([], {'suites': 'contrib non-free'}),
146 3: ([], {'suites': 'contrib non-free'}),
147 4: ([], {'suites': 'contrib non-free'}),
148 5: ([], {'suites': 'contrib non-free'}),
149 6: ([], {'suites': 'contrib non-free'})},
158 '6': ('Test caching with multiple apt-get updates.',
167 '7': ('Test pipelining of multiple simultaneous downloads.',
171 (1, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
172 'aspectj-doc', 'fop-doc', 'jswat-doc', 'asis-doc',
173 'bison-doc', 'crash-whitepaper', 'doc-iana',
174 'bash-doc', 'apt-howto-common', 'autotools-dev',
175 'aptitude-doc-en', 'armagetron-common', 'asr-manpages',
176 'atomix-data', 'alcovebook-sgml-doc', 'alamin-doc',
177 'aegis-doc', 'afbackup-common', 'airstrike-common',
181 '8': ('Test pipelining of multiple simultaneous downloads with many peers.',
190 (1, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
191 'aspectj-doc', 'fop-doc', 'jswat-doc', 'asis-doc',
192 'bison-doc', 'crash-whitepaper', 'doc-iana',
193 'bash-doc', 'apt-howto-common', 'autotools-dev',
194 'aptitude-doc-en', 'armagetron-common', 'asr-manpages',
195 'atomix-data', 'alcovebook-sgml-doc', 'alamin-doc',
196 'aegis-doc', 'afbackup-common', 'airstrike-common',
199 (2, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
200 'aspectj-doc', 'fop-doc', 'jswat-doc', 'asis-doc',
201 'bison-doc', 'crash-whitepaper', 'doc-iana',
202 'bash-doc', 'apt-howto-common', 'autotools-dev',
203 'aptitude-doc-en', 'armagetron-common', 'asr-manpages',
204 'atomix-data', 'alcovebook-sgml-doc', 'alamin-doc',
205 'aegis-doc', 'afbackup-common', 'airstrike-common',
208 (3, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
209 'aspectj-doc', 'fop-doc', 'jswat-doc', 'asis-doc',
210 'bison-doc', 'crash-whitepaper', 'doc-iana',
211 'bash-doc', 'apt-howto-common', 'autotools-dev',
212 'aptitude-doc-en', 'armagetron-common', 'asr-manpages',
213 'atomix-data', 'alcovebook-sgml-doc', 'alamin-doc',
214 'aegis-doc', 'afbackup-common', 'airstrike-common',
217 (4, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
218 'aspectj-doc', 'fop-doc', 'jswat-doc', 'asis-doc',
219 'bison-doc', 'crash-whitepaper', 'doc-iana',
220 'bash-doc', 'apt-howto-common', 'autotools-dev',
221 'aptitude-doc-en', 'armagetron-common', 'asr-manpages',
222 'atomix-data', 'alcovebook-sgml-doc', 'alamin-doc',
223 'aegis-doc', 'afbackup-common', 'airstrike-common',
226 (5, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
227 'aspectj-doc', 'fop-doc', 'jswat-doc', 'asis-doc',
228 'bison-doc', 'crash-whitepaper', 'doc-iana',
229 'bash-doc', 'apt-howto-common', 'autotools-dev',
230 'aptitude-doc-en', 'armagetron-common', 'asr-manpages',
231 'atomix-data', 'alcovebook-sgml-doc', 'alamin-doc',
232 'aegis-doc', 'afbackup-common', 'airstrike-common',
235 (6, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
236 'aspectj-doc', 'fop-doc', 'jswat-doc', 'asis-doc',
237 'bison-doc', 'crash-whitepaper', 'doc-iana',
238 'bash-doc', 'apt-howto-common', 'autotools-dev',
239 'aptitude-doc-en', 'armagetron-common', 'asr-manpages',
240 'atomix-data', 'alcovebook-sgml-doc', 'alamin-doc',
241 'aegis-doc', 'afbackup-common', 'airstrike-common',
245 '9': ('Start a single bootstrap and 6 downloaders and test downloading' +
246 ' a very large file.',
248 {1: {'clean': False},
255 (1, ['install', 'kde-icons-oxygen']),
257 (2, ['install', 'kde-icons-oxygen']),
259 (3, ['install', 'kde-icons-oxygen']),
261 (4, ['install', 'kde-icons-oxygen']),
263 (5, ['install', 'kde-icons-oxygen']),
265 (6, ['install', 'kde-icons-oxygen']),
270 assert 'all' not in tests
271 assert 'help' not in tests
274 apt_conf_template = """
276 // Location of the state dir
281 userstatus "status.user";
282 cdroms "cdroms.list";
285 // Location of the cache dir
286 Cache "var/cache/apt/" {
287 Archives "archives/";
288 srcpkgcache "srcpkgcache.bin";
289 pkgcache "pkgcache.bin";
294 SourceList "sources.list";
296 Preferences "preferences";
300 // Locations of binaries
302 methods "/usr/lib/apt/methods/";
305 dpkg "/usr/bin/dpkg --simulate";
306 dpkg-source "/usr/bin/dpkg-source";
307 dpkg-buildpackage "/usr/bin/dpkg-buildpackage";
308 apt-get "/usr/bin/apt-get";
309 apt-cache "/usr/bin/apt-cache";
313 /* Options you can set to see some debugging text They correspond to names
314 of classes in the source code */
317 pkgProblemResolver "false";
318 pkgDepCache::AutoInstall "false"; // what packages apt install to satify dependencies
320 pkgAcquire::Worker "false";
321 pkgAcquire::Auth "false";
323 pkgDPkgProgressReporting "false";
324 pkgOrderList "false";
327 pkgInitialize "false"; // This one will dump the configuration space
329 Acquire::Ftp "false"; // Show ftp command traffic
330 Acquire::Http "false"; // Show http command traffic
331 Acquire::gpgv "false"; // Show the gpgv traffic
332 aptcdrom "false"; // Show found package files
337 apt_p2p_conf_template = """
340 # Port to listen on for all requests (TCP and UDP)
343 # The rate to limit sending data to peers to, in KBytes/sec.
344 # Set this to 0 to not limit the upload bandwidth.
347 # Directory to store the downloaded files in
348 CACHE_DIR = %(CACHE_DIR)s
350 # Other directories containing packages to share with others
351 # WARNING: all files in these directories will be hashed and available
352 # for everybody to download
355 # User name to try and run as
358 # Whether it's OK to use an IP addres from a known local/private range
361 # Unload the packages cache after an interval of inactivity this long.
362 # The packages cache uses a lot of memory, and only takes a few seconds
363 # to reload when a new request arrives.
364 UNLOAD_PACKAGES_CACHE = 5m
366 # Refresh the DHT keys after this much time has passed.
367 # This should be a time slightly less than the DHT's KEY_EXPIRE value.
370 # Which DHT implementation to use.
371 # It must be possile to do "from <DHT>.DHT import DHT" to get a class that
372 # implements the IDHT interface.
373 DHT = apt_p2p_Khashmir
375 # Whether to only run the DHT (for providing only a bootstrap node)
376 DHT-ONLY = %(DHT-ONLY)s
379 # bootstrap nodes to contact to join the DHT
380 BOOTSTRAP = %(BOOTSTRAP)s
382 # whether this node is a bootstrap node
383 BOOTSTRAP_NODE = %(BOOTSTRAP_NODE)s
385 # Kademlia "K" constant, this should be an even number
388 # SHA1 is 160 bits long
391 # checkpoint every this many seconds
392 CHECKPOINT_INTERVAL = 5m
394 # concurrent xmlrpc calls per find node/value request!
397 # how many hosts to post to
400 # How many values to attempt to retrieve from the DHT.
401 # Setting this to 0 will try and get all values (which could take a while if
402 # a lot of nodes have values). Setting it negative will try to get that
403 # number of results from only the closest STORE_REDUNDANCY nodes to the hash.
404 # The default is a large negative number so all values from the closest
405 # STORE_REDUNDANCY nodes will be retrieved.
406 RETRIEVE_VALUES = -10000
408 # how many times in a row a node can fail to respond before it's booted from the routing table
411 # never ping a node more often than this
412 MIN_PING_INTERVAL = 15m
414 # refresh buckets that haven't been touched in this long
415 BUCKET_STALENESS = 1h
417 # expire entries older than this
420 # whether to spew info about the requests/responses in the protocol
425 """Remove all the files and directories below a top-level one.
428 @param top: the top-level directory to start at
432 for root, dirs, files in os.walk(top, topdown=False):
434 os.remove(os.path.join(root, name))
436 os.rmdir(os.path.join(root, name))
439 """Join together a list of directories into a path string.
441 @type dir: C{list} of C{string}
442 @param dir: the path to join together
444 @return: the joined together path
450 joined = os.path.join(joined, i)
454 """Create all the directories to make a path.
456 @type dir: C{list} of C{string}
457 @param dir: the path to create
460 if not os.path.exists(join(dir)):
461 os.makedirs(join(dir))
464 """Create an empty file.
466 @type path: C{list} of C{string}
467 @param path: the path to create
471 f = open(join(path), 'w')
474 def start(cmd, args, work_dir = None):
475 """Fork and start a background process running.
478 @param cmd: the name of the command to run
479 @type args: C{list} of C{string}
480 @param args: the argument to pass to the command
481 @type work_dir: C{string}
482 @param work_dir: the directory to change to to execute the child process in
483 (optional, defaults to the current directory)
485 @return: the PID of the forked process
489 new_cmd = [cmd] + args
490 pid = os.spawnvp(os.P_NOWAIT, new_cmd[0], new_cmd)
494 """Stop a forked background process that is running.
497 @param pid: the PID of the process to stop
499 @return: the return status code from the child
503 # First try a keyboard interrupt
504 os.kill(pid, signal.SIGINT)
507 (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
511 # Try a keyboard interrupt again, just in case
512 os.kill(pid, signal.SIGINT)
515 (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
520 os.kill(pid, signal.SIGTERM)
523 (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
527 # Finally a kill, don't return until killed
528 os.kill(pid, signal.SIGKILL)
531 (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
535 def apt_get(num_down, cmd):
536 """Start an apt-get process in the background.
538 The default argument specified to the apt-get invocation are
539 'apt-get -d -q -c <conf_file>'. Any additional arguments (including
540 the apt-get action to use) should be specified.
542 @type num_down: C{int}
543 @param num_down: the number of the downloader to use
544 @type cmd: C{list} of C{string}
545 @param cmd: the arguments to pass to the apt-get process
547 @return: the PID of the background process
551 print '*************** apt-get (' + str(num_down) + ') ' + ' '.join(cmd) + ' ****************'
552 apt_conf = join([down_dir(num_down), 'etc', 'apt', 'apt.conf'])
553 dpkg_status = join([down_dir(num_down), 'var', 'lib', 'dpkg', 'status'])
554 args = ['-d', '-c', apt_conf, '-o', 'Dir::state::status='+dpkg_status] + cmd
555 pid = start('apt-get', args)
558 def bootstrap_address(num_boot):
559 """Determine the bootstrap address to use for a node.
561 @type num_boot: C{int}
562 @param num_boot: the number of the bootstrap node
564 @return: the bootstrap address to use
568 return 'localhost:1' + str(num_boot) + '969'
570 def down_dir(num_down):
571 """Determine the working directory to use for a downloader.
573 @type num_down: C{int}
574 @param num_down: the number of the downloader
576 @return: the downloader's directory
580 return os.path.join(CWD,'downloader' + str(num_down))
582 def boot_dir(num_boot):
583 """Determine the working directory to use for a bootstrap node.
585 @type num_boot: C{int}
586 @param num_boot: the number of the bootstrap node
588 @return: the bootstrap node's directory
592 return os.path.join(CWD,'bootstrap' + str(num_boot))
594 def start_downloader(bootstrap_addresses, num_down, options = {},
595 mirror = 'ftp.us.debian.org/debian',
596 suites = 'main contrib non-free', clean = True):
597 """Initialize a new downloader process.
599 The default arguments specified to the downloader invocation are
600 the configuration directory, apt port, minport, maxport and the
602 Any additional arguments needed should be specified by L{options}.
604 @type num_down: C{int}
605 @param num_down: the number of the downloader to use
606 @type options: C{dictionary}
607 @param options: the dictionary of string formatting values for creating
608 the apt-p2p configuration file (see L{apt_p2p_conf_template} above).
609 (optional, defaults to only using the default arguments)
610 @type mirror: C{string}
611 @param mirror: the Debian mirror to use
612 (optional, defaults to 'ftp.us.debian.org/debian')
613 @type suites: C{string}
614 @param suites: space separated list of suites to download
615 (optional, defaults to 'main contrib non-free')
616 @type clean: C{boolean}
617 @param clean: whether to remove any previous downloader files
618 (optional, defaults to removing them)
620 @return: the PID of the downloader process
624 assert num_down < 100
626 print '************************** Starting Downloader ' + str(num_down) + ' **************************'
628 downloader_dir = down_dir(num_down)
636 # Create the directory structure needed by apt
637 makedirs([downloader_dir, 'etc', 'apt', 'apt.conf.d'])
638 makedirs([downloader_dir, 'var', 'lib', 'apt', 'lists', 'partial'])
639 makedirs([downloader_dir, 'var', 'lib', 'dpkg'])
640 makedirs([downloader_dir, 'var', 'cache', 'apt', 'archives', 'partial'])
641 touch([downloader_dir, 'var', 'lib', 'apt', 'lists', 'lock'])
642 touch([downloader_dir, 'var', 'lib', 'dpkg', 'lock'])
643 touch([downloader_dir, 'var', 'lib', 'dpkg', 'status'])
644 touch([downloader_dir, 'var', 'cache', 'apt', 'archives', 'lock'])
646 # Create apt's config files
647 f = open(join([downloader_dir, 'etc', 'apt', 'sources.list']), 'w')
648 f.write('deb http://localhost:1%02d77/%s/ unstable %s\n' % (num_down, mirror, suites))
651 f = open(join([downloader_dir, 'etc', 'apt', 'apt.conf']), 'w')
652 f.write('Dir "' + downloader_dir + '"')
653 f.write(apt_conf_template)
656 defaults = {'PORT': '1%02d77' % num_down,
657 'CACHE_DIR': downloader_dir,
659 'BOOTSTRAP': bootstrap_addresses,
660 'BOOTSTRAP_NODE': 'no'}
663 defaults[k] = options[k]
664 f = open(join([downloader_dir, 'apt-p2p.conf']), 'w')
665 f.write(apt_p2p_conf_template % defaults)
668 pid = start('python', [join([sys.path[0], 'apt-p2p.py']),
669 '--config-file=' + join([downloader_dir, 'apt-p2p.conf']),
670 '--log-file=' + join([downloader_dir, 'apt-p2p.log']),],
674 def start_bootstrap(bootstrap_addresses, num_boot, options = [], clean = True):
675 """Initialize a new bootstrap node process.
677 The default arguments specified to the apt-p2p invocation are
678 the state file and port to use. Any additional arguments needed
679 should be specified by L{options}.
681 @type num_boot: C{int}
682 @param num_boot: the number of the bootstrap node to use
683 @type options: C{list} of C{string}
684 @param options: the arguments to pass to the bootstrap node
685 (optional, defaults to only using the default arguments)
686 @type clean: C{boolean}
687 @param clean: whether to remove any previous bootstrap node files
688 (optional, defaults to removing them)
690 @return: the PID of the downloader process
696 print '************************** Starting Bootstrap ' + str(num_boot) + ' **************************'
698 bootstrap_dir = boot_dir(num_boot)
706 makedirs([bootstrap_dir])
708 defaults = {'PORT': '1%d969' % num_boot,
709 'CACHE_DIR': bootstrap_dir,
711 'BOOTSTRAP': bootstrap_addresses,
712 'BOOTSTRAP_NODE': 'yes'}
715 defaults[k] = options[k]
716 f = open(join([bootstrap_dir, 'apt-p2p.conf']), 'w')
717 f.write(apt_p2p_conf_template % defaults)
720 pid = start('python', [join([sys.path[0], 'apt-p2p.py']),
721 '--config-file=' + join([bootstrap_dir, 'apt-p2p.conf']),
722 '--log-file=' + join([bootstrap_dir, 'apt-p2p.log']),],
727 def run_test(bootstraps, downloaders, apt_get_queue):
728 """Run a single test.
730 @type bootstraps: C{dictionary} of {C{int}: C{list} of C{string}}
731 @param bootstraps: the bootstrap nodes to start, keys are the bootstrap numbers and
732 values are the list of options to invoke the bootstrap node with
733 @type downloaders: C{dictionary} of {C{int}: (C{int}, C{list} of C{string})}
734 @param downloaders: the downloaders to start, keys are the downloader numbers and
735 values are the list of options to invoke the downloader with
736 @type apt_get_queue: C{list} of (C{int}, C{list} of C{string})
737 @param apt_get_queue: the apt-get downloader to use and commands to execute
738 @rtype: C{list} of (C{float}, C{int})
739 @return: the execution time and returned status code for each element of apt_get_queue
743 running_bootstraps = {}
744 running_downloaders = {}
749 boot_keys = bootstraps.keys()
751 bootstrap_addresses = bootstrap_address(boot_keys[0])
752 for i in xrange(1, len(boot_keys)):
753 bootstrap_addresses += '\n ' + bootstrap_address(boot_keys[i])
755 for k, v in bootstraps.items():
756 running_bootstraps[k] = start_bootstrap(bootstrap_addresses, k, **v)
760 for k, v in downloaders.items():
761 running_downloaders[k] = start_downloader(bootstrap_addresses, k, **v)
765 for (num_down, cmd) in apt_get_queue:
766 running_apt_get[num_down] = apt_get(num_down, cmd)
768 (pid, r_value) = os.waitpid(running_apt_get[num_down], 0)
769 elapsed = time() - start_time
770 del running_apt_get[num_down]
771 r_value = r_value / 256
772 apt_get_results.append((elapsed, r_value))
775 print '********** apt-get completed successfully in ' + str(elapsed) + ' sec. *****************'
777 print '********** apt-get finished with status ' + str(r_value) + ' in ' + str(elapsed) + ' sec. ************'
782 print '************************** Exception occurred **************************'
784 print '************************** will attempt to shut down *******************'
786 print '*********************** shutting down the apt-gets *******************'
787 for k, v in running_apt_get.items():
789 print 'apt-get', k, stop(v)
791 print '************************** Exception occurred **************************'
796 print '*********************** shutting down the downloaders *******************'
797 for k, v in running_downloaders.items():
799 print 'downloader', k, stop(v)
801 print '************************** Exception occurred **************************'
806 print '************************** shutting down the bootstraps *******************'
807 for k, v in running_bootstraps.items():
809 print 'bootstrap', k, stop(v)
811 print '************************** Exception occurred **************************'
814 print '************************** Test Results *******************'
816 for (num_down, cmd) in apt_get_queue:
818 s = str(num_down) + ': "apt-get ' + ' '.join(cmd) + '" '
819 if len(apt_get_results) > i:
820 (elapsed, r_value) = apt_get_results[i]
821 s += 'took ' + str(elapsed) + ' secs (' + str(r_value) + ')'
823 s += 'did not complete'
826 return apt_get_results
829 """Get the usage information to display to the user.
832 @return: the usage information to display
836 s = 'Usage: ' + sys.argv[0] + ' (all|<test>|help)\n\n'
837 s += ' all - run all the tests\n'
838 s += ' help - display this usage information\n'
839 s += ' <test> - run the <test> test (see list below for valid tests)\n\n'
844 s += 'test "' + str(k) + '" - ' + v[0] + '\n'
848 if __name__ == '__main__':
849 if len(sys.argv) != 2:
851 elif sys.argv[1] == 'all':
852 for k, v in tests.items():
853 run_test(v[1], v[2], v[3])
854 elif sys.argv[1] in tests:
855 v = tests[sys.argv[1]]
856 run_test(v[1], v[2], v[3])
857 elif sys.argv[1] == 'help':
860 print 'Unknown test to run:', sys.argv[1], '\n'