Multiple peer downloading is mostly working now.
[quix0rs-apt-p2p.git] / test.py
1 #!/usr/bin/env python
2
3 """Automated tests of the apt-p2p functionality.
4
5 This script runs several automatic tests of some of the functionality in
6 the apt-p2p program.
7
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}).
15     
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).
19     
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).
23     
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.
27     
28 @type CWD: C{string}
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
32
33 """
34
35 from time import sleep, time
36 import sys, os, signal
37 from traceback import print_exc
38 from os.path import exists
39
40 tests = {'1': ('Start a single bootstrap and downloader, test updating and downloading ' +
41              'using HTTP only.',
42              {1: {}},
43              {1: {}},
44              [(1, ['update']), 
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']),
55               ]),
56
57          '2': ('Start a single bootstrap and 2 downloaders to test downloading from a peer.',
58                {1: {}},
59                {1: {},
60                 2: {}},
61                [(1, ['update']),
62                 (2, ['update']),
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']),
77                 ]),
78                 
79          '3': ('Start a single bootstrap and 6 downloaders, to test downloading' +
80                ' speeds from each other.',
81                {1: {}},
82                {1: {},
83                 2: {},
84                 3: {},
85                 4: {},
86                 5: {},
87                 6: {}},
88                [(1, ['update']),
89                 (2, ['update']),
90                 (3, ['update']),
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']),
103                 (4, ['update']),
104                 (4, ['install', 'aboot-base']),
105                 (4, ['install', 'ada-reference-manual']),
106                 (4, ['install', 'fop-doc']),
107                 (4, ['install', 'doc-iana']),
108                 (5, ['update']),
109                 (5, ['install', 'aboot-base']),
110                 (5, ['install', 'ada-reference-manual']),
111                 (5, ['install', 'fop-doc']),
112                 (5, ['install', 'doc-iana']),
113                 (6, ['update']),
114                 (6, ['install', 'aboot-base']),
115                 (6, ['install', 'ada-reference-manual']),
116                 (6, ['install', 'fop-doc']),
117                 (6, ['install', 'doc-iana']),
118                 ]),
119
120          '4': ('Start a single bootstrap and 1 downloader, requesting the same' +
121                ' packages multiple times to test caching.',
122                {1: {}},
123                {1: {}},
124                [(1, ['update']),
125                 (1, ['install', 'aboot-base']),
126                 (1, ['install', 'ada-reference-manual']),
127                 (1, ['install', 'fop-doc']),
128                 (1, ['install', 'doc-iana']),
129                 (1, ['update']),
130                 (1, ['install', 'aboot-base']),
131                 (1, ['install', 'ada-reference-manual']),
132                 (1, ['install', 'fop-doc']),
133                 (1, ['install', 'doc-iana']),
134                 (1, ['update']),
135                 (1, ['install', 'aboot-base']),
136                 (1, ['install', 'ada-reference-manual']),
137                 (1, ['install', 'fop-doc']),
138                 (1, ['install', 'doc-iana']),
139                 ]),
140                 
141          '5': ('Start a single bootstrap and 6 downloaders, update all to test' +
142                ' that they can all see each other.',
143                {1: {}},
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'})},
150                [(1, ['update']),
151                 (2, ['update']),
152                 (3, ['update']),
153                 (4, ['update']),
154                 (5, ['update']),
155                 (6, ['update']),
156                 ]),
157
158         '6': ('Test caching with multiple apt-get updates.',
159              {1: {}},
160              {1: {}},
161              [(1, ['update']), 
162               (1, ['update']),
163               (1, ['update']),
164               (1, ['update']),
165               ]),
166
167         '7': ('Test pipelining of multiple simultaneous downloads.',
168              {1: {}},
169              {1: {}},
170              [(1, ['update']), 
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',
178                    ]),
179               ]),
180
181         '8': ('Test pipelining of multiple simultaneous downloads with many peers.',
182              {1: {}},
183              {1: {},
184               2: {},
185               3: {},
186               4: {},
187               5: {},
188               6: {}},
189              [(1, ['update']), 
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',
197                    ]),
198               (2, ['update']), 
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',
206                    ]),
207               (3, ['update']), 
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',
215                    ]),
216               (4, ['update']), 
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',
224                    ]),
225               (5, ['update']), 
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',
233                    ]),
234               (6, ['update']), 
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',
242                    ]),
243               ]),
244
245          '9': ('Start a single bootstrap and 6 downloaders and test downloading' +
246                ' a very large file.',
247                {1: {}},
248                {1: {'clean': False},
249                 2: {'clean': False},
250                 3: {},
251                 4: {},
252                 5: {},
253                 6: {}},
254                [(1, ['update']),
255                 (1, ['install', 'kde-icons-oxygen']),
256                 (2, ['update']),
257                 (2, ['install', 'kde-icons-oxygen']),
258                 (3, ['update']),
259                 (3, ['install', 'kde-icons-oxygen']),
260                 (4, ['update']),
261                 (4, ['install', 'kde-icons-oxygen']),
262                 (5, ['update']),
263                 (5, ['install', 'kde-icons-oxygen']),
264                 (6, ['update']),
265                 (6, ['install', 'kde-icons-oxygen']),
266                 ]),
267
268          }
269
270 assert 'all' not in tests
271 assert 'help' not in tests
272
273 CWD = os.getcwd()
274 apt_conf_template = """
275 {
276   // Location of the state dir
277   State "var/lib/apt/"
278   {
279      Lists "lists/";
280      xstatus "xstatus";
281      userstatus "status.user";
282      cdroms "cdroms.list";
283   };
284
285   // Location of the cache dir
286   Cache "var/cache/apt/" {
287      Archives "archives/";
288      srcpkgcache "srcpkgcache.bin";
289      pkgcache "pkgcache.bin";
290   };
291
292   // Config files
293   Etc "etc/apt/" {
294      SourceList "sources.list";
295      Main "apt.conf";
296      Preferences "preferences";
297      Parts "apt.conf.d/";
298   };
299
300   // Locations of binaries
301   Bin {
302      methods "/usr/lib/apt/methods/";
303      gzip "/bin/gzip";
304      gpg  "/usr/bin/gpgv";
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";
310   };
311 };
312
313 /* Options you can set to see some debugging text They correspond to names
314    of classes in the source code */
315 Debug
316 {
317   pkgProblemResolver "false";
318   pkgDepCache::AutoInstall "false"; // what packages apt install to satify dependencies
319   pkgAcquire "false";
320   pkgAcquire::Worker "false";
321   pkgAcquire::Auth "false";
322   pkgDPkgPM "false";
323   pkgDPkgProgressReporting "false";
324   pkgOrderList "false";
325   BuildDeps "false";
326
327   pkgInitialize "false";   // This one will dump the configuration space
328   NoLocking "false";
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
333   IdentCdrom "false";
334
335 }
336 """
337 apt_p2p_conf_template = """
338 [DEFAULT]
339
340 # Port to listen on for all requests (TCP and UDP)
341 PORT = %(PORT)s
342     
343 # The rate to limit sending data to peers to, in KBytes/sec.
344 # Set this to 0 to not limit the upload bandwidth.
345 UPLOAD_LIMIT = 100
346
347 # Directory to store the downloaded files in
348 CACHE_DIR = %(CACHE_DIR)s
349     
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
353 # OTHER_DIRS = 
354     
355 # User name to try and run as
356 # USERNAME = 
357
358 # Whether it's OK to use an IP addres from a known local/private range
359 LOCAL_OK = yes
360
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
365
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.
368 KEY_REFRESH = 57m
369
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
374
375 # Whether to only run the DHT (for providing only a bootstrap node)
376 DHT-ONLY = %(DHT-ONLY)s
377
378 [apt_p2p_Khashmir]
379 # bootstrap nodes to contact to join the DHT
380 BOOTSTRAP = %(BOOTSTRAP)s
381
382 # whether this node is a bootstrap node
383 BOOTSTRAP_NODE = %(BOOTSTRAP_NODE)s
384
385 # Kademlia "K" constant, this should be an even number
386 K = 8
387
388 # SHA1 is 160 bits long
389 HASH_LENGTH = 160
390
391 # checkpoint every this many seconds
392 CHECKPOINT_INTERVAL = 5m
393
394 # concurrent xmlrpc calls per find node/value request!
395 CONCURRENT_REQS = 4
396
397 # how many hosts to post to
398 STORE_REDUNDANCY = 3
399
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
407
408 # how many times in a row a node can fail to respond before it's booted from the routing table
409 MAX_FAILURES = 3
410
411 # never ping a node more often than this
412 MIN_PING_INTERVAL = 15m
413
414 # refresh buckets that haven't been touched in this long
415 BUCKET_STALENESS = 1h
416
417 # expire entries older than this
418 KEY_EXPIRE = 1h
419
420 # whether to spew info about the requests/responses in the protocol
421 SPEW = yes
422 """
423
424 def rmrf(top):
425     """Remove all the files and directories below a top-level one.
426     
427     @type top: C{string}
428     @param top: the top-level directory to start at
429     
430     """
431     
432     for root, dirs, files in os.walk(top, topdown=False):
433         for name in files:
434             os.remove(os.path.join(root, name))
435         for name in dirs:
436             os.rmdir(os.path.join(root, name))
437
438 def join(dir):
439     """Join together a list of directories into a path string.
440     
441     @type dir: C{list} of C{string}
442     @param dir: the path to join together
443     @rtype: C{string}
444     @return: the joined together path
445     
446     """
447     
448     joined = ''
449     for i in dir:
450         joined = os.path.join(joined, i)
451     return joined
452
453 def makedirs(dir):
454     """Create all the directories to make a path.
455     
456     @type dir: C{list} of C{string}
457     @param dir: the path to create
458     
459     """
460     if not os.path.exists(join(dir)):
461         os.makedirs(join(dir))
462
463 def touch(path):
464     """Create an empty file.
465     
466     @type path: C{list} of C{string}
467     @param path: the path to create
468     
469     """
470     
471     f = open(join(path), 'w')
472     f.close()
473
474 def start(cmd, args, work_dir = None):
475     """Fork and start a background process running.
476     
477     @type cmd: C{string}
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)
484     @rtype: C{int}
485     @return: the PID of the forked process
486     
487     """
488     
489     new_cmd = [cmd] + args
490     pid = os.spawnvp(os.P_NOWAIT, new_cmd[0], new_cmd)
491     return pid
492
493 def stop(pid):
494     """Stop a forked background process that is running.
495     
496     @type pid: C{int}
497     @param pid: the PID of the process to stop
498     @rtype: C{int}
499     @return: the return status code from the child
500     
501     """
502
503     # First try a keyboard interrupt
504     os.kill(pid, signal.SIGINT)
505     for i in xrange(5):
506         sleep(1)
507         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
508         if r_pid:
509             return r_value
510     
511     # Try a keyboard interrupt again, just in case
512     os.kill(pid, signal.SIGINT)
513     for i in xrange(5):
514         sleep(1)
515         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
516         if r_pid:
517             return r_value
518
519     # Try a terminate
520     os.kill(pid, signal.SIGTERM)
521     for i in xrange(5):
522         sleep(1)
523         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
524         if r_pid:
525             return r_value
526
527     # Finally a kill, don't return until killed
528     os.kill(pid, signal.SIGKILL)
529     while not r_pid:
530         sleep(1)
531         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
532
533     return r_value
534
535 def apt_get(num_down, cmd):
536     """Start an apt-get process in the background.
537
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.
541     
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
546     @rtype: C{int}
547     @return: the PID of the background process
548     
549     """
550     
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)
556     return pid
557
558 def bootstrap_address(num_boot):
559     """Determine the bootstrap address to use for a node.
560     
561     @type num_boot: C{int}
562     @param num_boot: the number of the bootstrap node
563     @rtype: C{string}
564     @return: the bootstrap address to use
565     
566     """
567     
568     return 'localhost:1' + str(num_boot) + '969'
569
570 def down_dir(num_down):
571     """Determine the working directory to use for a downloader.
572     
573     @type num_down: C{int}
574     @param num_down: the number of the downloader
575     @rtype: C{string}
576     @return: the downloader's directory
577     
578     """
579     
580     return os.path.join(CWD,'downloader' + str(num_down))
581
582 def boot_dir(num_boot):
583     """Determine the working directory to use for a bootstrap node.
584     
585     @type num_boot: C{int}
586     @param num_boot: the number of the bootstrap node
587     @rtype: C{string}
588     @return: the bootstrap node's directory
589     
590     """
591     
592     return os.path.join(CWD,'bootstrap' + str(num_boot))
593
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.
598
599     The default arguments specified to the downloader invocation are
600     the configuration directory, apt port, minport, maxport and the
601     maximum upload rate. 
602     Any additional arguments needed should be specified by L{options}.
603     
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)
619     @rtype: C{int}
620     @return: the PID of the downloader process
621     
622     """
623     
624     assert num_down < 100
625     
626     print '************************** Starting Downloader ' + str(num_down) + ' **************************'
627
628     downloader_dir = down_dir(num_down)
629     
630     if clean:
631         try:
632             rmrf(downloader_dir)
633         except:
634             pass
635     
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'])
645
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))
649         f.close()
650
651         f = open(join([downloader_dir, 'etc', 'apt', 'apt.conf']), 'w')
652         f.write('Dir "' + downloader_dir + '"')
653         f.write(apt_conf_template)
654         f.close()
655
656     defaults = {'PORT': '1%02d77' % num_down,
657                 'CACHE_DIR': downloader_dir,
658                 'DHT-ONLY': 'no',
659                 'BOOTSTRAP': bootstrap_addresses,
660                 'BOOTSTRAP_NODE': 'no'}
661
662     for k in options:
663         defaults[k] = options[k]
664     f = open(join([downloader_dir, 'apt-p2p.conf']), 'w')
665     f.write(apt_p2p_conf_template % defaults)
666     f.close()
667     
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']),],
671                 downloader_dir)
672     return pid
673
674 def start_bootstrap(bootstrap_addresses, num_boot, options = [], clean = True):
675     """Initialize a new bootstrap node process.
676
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}.
680     
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)
689     @rtype: C{int}
690     @return: the PID of the downloader process
691     
692     """
693     
694     assert num_boot < 10
695
696     print '************************** Starting Bootstrap ' + str(num_boot) + ' **************************'
697
698     bootstrap_dir = boot_dir(num_boot)
699     
700     if clean:
701         try:
702             rmrf(bootstrap_dir)
703         except:
704             pass
705
706     makedirs([bootstrap_dir])
707
708     defaults = {'PORT': '1%d969' % num_boot,
709                 'CACHE_DIR': bootstrap_dir,
710                 'DHT-ONLY': 'yes',
711                 'BOOTSTRAP': bootstrap_addresses,
712                 'BOOTSTRAP_NODE': 'yes'}
713
714     for k in options:
715         defaults[k] = options[k]
716     f = open(join([bootstrap_dir, 'apt-p2p.conf']), 'w')
717     f.write(apt_p2p_conf_template % defaults)
718     f.close()
719     
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']),],
723                 bootstrap_dir)
724
725     return pid
726
727 def run_test(bootstraps, downloaders, apt_get_queue):
728     """Run a single test.
729     
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
740     
741     """
742     
743     running_bootstraps = {}
744     running_downloaders = {}
745     running_apt_get = {}
746     apt_get_results = []
747
748     try:
749         boot_keys = bootstraps.keys()
750         boot_keys.sort()
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])
754             
755         for k, v in bootstraps.items():
756             running_bootstraps[k] = start_bootstrap(bootstrap_addresses, k, **v)
757         
758         sleep(5)
759         
760         for k, v in downloaders.items():
761             running_downloaders[k] = start_downloader(bootstrap_addresses, k, **v)
762     
763         sleep(5)
764         
765         for (num_down, cmd) in apt_get_queue:
766             running_apt_get[num_down] = apt_get(num_down, cmd)
767             start_time = time()
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))
773
774             if r_value == 0:
775                 print '********** apt-get completed successfully in ' +  str(elapsed) + ' sec. *****************'
776             else:
777                 print '********** apt-get finished with status ' + str(r_value) + ' in ' +  str(elapsed) + ' sec. ************'
778         
779             sleep(5)
780             
781     except:
782         print '************************** Exception occurred **************************'
783         print_exc()
784         print '************************** will attempt to shut down *******************'
785         
786     print '*********************** shutting down the apt-gets *******************'
787     for k, v in running_apt_get.items():
788         try:
789             print 'apt-get', k, stop(v)
790         except:
791             print '************************** Exception occurred **************************'
792             print_exc()
793
794     sleep(5)
795
796     print '*********************** shutting down the downloaders *******************'
797     for k, v in running_downloaders.items():
798         try:
799             print 'downloader', k, stop(v)
800         except:
801             print '************************** Exception occurred **************************'
802             print_exc()
803
804     sleep(5)
805
806     print '************************** shutting down the bootstraps *******************'
807     for k, v in running_bootstraps.items():
808         try:
809             print 'bootstrap', k, stop(v)
810         except:
811             print '************************** Exception occurred **************************'
812             print_exc()
813
814     print '************************** Test Results *******************'
815     i = -1
816     for (num_down, cmd) in apt_get_queue:
817         i += 1
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) + ')'
822         else:
823             s += 'did not complete'
824         print s
825     
826     return apt_get_results
827
828 def get_usage():
829     """Get the usage information to display to the user.
830     
831     @rtype: C{string}
832     @return: the usage information to display
833     
834     """
835     
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'
840     
841     t = tests.items()
842     t.sort()
843     for k, v in t:
844         s += 'test "' + str(k) + '" - ' + v[0] + '\n'
845     
846     return s
847
848 if __name__ == '__main__':
849     if len(sys.argv) != 2:
850         print get_usage()
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':
858         print get_usage()
859     else:
860         print 'Unknown test to run:', sys.argv[1], '\n'
861         print get_usage()
862