Always download max PIECE_SIZE from peers, even if there's only one peer.
[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               ]),
55
56          '2': ('Start a single bootstrap and 2 downloaders to test downloading from a peer.',
57                {1: {}},
58                {1: {},
59                 2: {}},
60                [(1, ['update']),
61                 (2, ['update']),
62                 (1, ['install', 'aboot-base']),
63                 (2, ['install', 'aboot-base']),
64                 (1, ['install', 'aap-doc']),
65                 (1, ['install', 'ada-reference-manual']),
66                 (1, ['install', 'fop-doc']),
67                 (1, ['install', 'jswat-doc']),
68                 (1, ['install', 'bison-doc']),
69                 (1, ['install', 'crash-whitepaper']),
70                 (2, ['install', 'aap-doc']),
71                 (2, ['install', 'ada-reference-manual']),
72                 (2, ['install', 'fop-doc']),
73                 (2, ['install', 'jswat-doc']),
74                 (2, ['install', 'bison-doc']),
75                 (2, ['install', 'crash-whitepaper']),
76                 ]),
77                 
78          '3': ('Start a single bootstrap and 6 downloaders, to test downloading' +
79                ' speeds from each other.',
80                {1: {}},
81                {1: {},
82                 2: {},
83                 3: {},
84                 4: {},
85                 5: {},
86                 6: {}},
87                [(1, ['update']),
88                 (1, ['install', 'aboot-base']),
89                 (1, ['install', 'ada-reference-manual']),
90                 (1, ['install', 'fop-doc']),
91                 (1, ['install', 'crash-whitepaper']),
92                 (2, ['update']),
93                 (2, ['install', 'aboot-base']),
94                 (2, ['install', 'ada-reference-manual']),
95                 (2, ['install', 'fop-doc']),
96                 (2, ['install', 'crash-whitepaper']),
97                 (3, ['update']),
98                 (3, ['install', 'aboot-base']),
99                 (3, ['install', 'ada-reference-manual']),
100                 (3, ['install', 'fop-doc']),
101                 (3, ['install', 'crash-whitepaper']),
102                 (4, ['update']),
103                 (4, ['install', 'aboot-base']),
104                 (4, ['install', 'ada-reference-manual']),
105                 (4, ['install', 'fop-doc']),
106                 (4, ['install', 'crash-whitepaper']),
107                 (5, ['update']),
108                 (5, ['install', 'aboot-base']),
109                 (5, ['install', 'ada-reference-manual']),
110                 (5, ['install', 'fop-doc']),
111                 (5, ['install', 'crash-whitepaper']),
112                 (6, ['update']),
113                 (6, ['install', 'aboot-base']),
114                 (6, ['install', 'ada-reference-manual']),
115                 (6, ['install', 'fop-doc']),
116                 (6, ['install', 'crash-whitepaper']),
117                 ]),
118
119          '4': ('Start a single bootstrap and 1 downloader, requesting the same' +
120                ' packages multiple times to test caching.',
121                {1: {}},
122                {1: {}},
123                [(1, ['update']),
124                 (1, ['install', 'aboot-base']),
125                 (1, ['install', 'ada-reference-manual']),
126                 (1, ['install', 'fop-doc']),
127                 (1, ['install', 'crash-whitepaper']),
128                 (1, ['update']),
129                 (1, ['install', 'aboot-base']),
130                 (1, ['install', 'ada-reference-manual']),
131                 (1, ['install', 'fop-doc']),
132                 (1, ['install', 'crash-whitepaper']),
133                 (1, ['update']),
134                 (1, ['install', 'aboot-base']),
135                 (1, ['install', 'ada-reference-manual']),
136                 (1, ['install', 'fop-doc']),
137                 (1, ['install', 'crash-whitepaper']),
138                 ]),
139                 
140          '5': ('Start a single bootstrap and 6 downloaders, update all to test' +
141                ' that they can all see each other.',
142                {1: {}},
143                {1: ([], {'suites': 'contrib non-free'}),
144                 2: ([], {'suites': 'contrib non-free'}),
145                 3: ([], {'suites': 'contrib non-free'}),
146                 4: ([], {'suites': 'contrib non-free'}),
147                 5: ([], {'suites': 'contrib non-free'}),
148                 6: ([], {'suites': 'contrib non-free'})},
149                [(1, ['update']),
150                 (2, ['update']),
151                 (3, ['update']),
152                 (4, ['update']),
153                 (5, ['update']),
154                 (6, ['update']),
155                 ]),
156
157         '6': ('Test caching with multiple apt-get updates.',
158              {1: {}},
159              {1: {}},
160              [(1, ['update']), 
161               (1, ['update']),
162               (1, ['update']),
163               (1, ['update']),
164               ]),
165
166         '7': ('Test pipelining of multiple simultaneous downloads.',
167              {1: {}},
168              {1: {}},
169              [(1, ['update']), 
170               (1, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
171                    'aspectj-doc', 'fop-doc', 'asis-doc',
172                    'bison-doc', 'crash-whitepaper',
173                    'bash-doc', 'apt-howto-common', 'autotools-dev',
174                    'aptitude-doc-en', 'asr-manpages',
175                    'atomix-data', 'alcovebook-sgml-doc',
176                    'afbackup-common', 'airstrike-common',
177                    ]),
178               ]),
179
180         '8': ('Test pipelining of multiple simultaneous downloads with many peers.',
181              {1: {}},
182              {1: {},
183               2: {},
184               3: {},
185               4: {},
186               5: {},
187               6: {}},
188              [(1, ['update']), 
189               (1, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
190                    'aspectj-doc', 'fop-doc', 'asis-doc',
191                    'bison-doc', 'crash-whitepaper',
192                    'bash-doc', 'apt-howto-common', 'autotools-dev',
193                    'aptitude-doc-en', 'asr-manpages',
194                    'atomix-data', 'alcovebook-sgml-doc',
195                    'afbackup-common', 'airstrike-common',
196                    ]),
197               (2, ['update']), 
198               (2, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
199                    'aspectj-doc', 'fop-doc', 'asis-doc',
200                    'bison-doc', 'crash-whitepaper',
201                    'bash-doc', 'apt-howto-common', 'autotools-dev',
202                    'aptitude-doc-en', 'asr-manpages',
203                    'atomix-data', 'alcovebook-sgml-doc',
204                    'afbackup-common', 'airstrike-common',
205                    ]),
206               (3, ['update']), 
207               (3, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
208                    'aspectj-doc', 'fop-doc', 'asis-doc',
209                    'bison-doc', 'crash-whitepaper',
210                    'bash-doc', 'apt-howto-common', 'autotools-dev',
211                    'aptitude-doc-en', 'asr-manpages',
212                    'atomix-data', 'alcovebook-sgml-doc',
213                    'afbackup-common', 'airstrike-common',
214                    ]),
215               (4, ['update']), 
216               (4, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
217                    'aspectj-doc', 'fop-doc', 'asis-doc',
218                    'bison-doc', 'crash-whitepaper',
219                    'bash-doc', 'apt-howto-common', 'autotools-dev',
220                    'aptitude-doc-en', 'asr-manpages',
221                    'atomix-data', 'alcovebook-sgml-doc',
222                    'afbackup-common', 'airstrike-common',
223                    ]),
224               (5, ['update']), 
225               (5, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
226                    'aspectj-doc', 'fop-doc', 'asis-doc',
227                    'bison-doc', 'crash-whitepaper',
228                    'bash-doc', 'apt-howto-common', 'autotools-dev',
229                    'aptitude-doc-en', 'asr-manpages',
230                    'atomix-data', 'alcovebook-sgml-doc',
231                    'afbackup-common', 'airstrike-common',
232                    ]),
233               (6, ['update']), 
234               (6, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
235                    'aspectj-doc', 'fop-doc', 'asis-doc',
236                    'bison-doc', 'crash-whitepaper',
237                    'bash-doc', 'apt-howto-common', 'autotools-dev',
238                    'aptitude-doc-en', 'asr-manpages',
239                    'atomix-data', 'alcovebook-sgml-doc',
240                    'afbackup-common', 'airstrike-common',
241                    ]),
242               ]),
243
244          '9': ('Start a single bootstrap and 6 downloaders and test downloading' +
245                ' a very large file.',
246                {1: {}},
247                {1: {},
248                 2: {},
249                 3: {},
250                 4: {},
251                 5: {},
252                 6: {}},
253                [(1, ['update']),
254                 (1, ['install', 'kde-icons-oxygen']),
255                 (2, ['update']),
256                 (2, ['install', 'kde-icons-oxygen']),
257                 (3, ['update']),
258                 (3, ['install', 'kde-icons-oxygen']),
259                 (4, ['update']),
260                 (4, ['install', 'kde-icons-oxygen']),
261                 (5, ['update']),
262                 (5, ['install', 'kde-icons-oxygen']),
263                 (6, ['update']),
264                 (6, ['install', 'kde-icons-oxygen']),
265                 ]),
266
267          }
268
269 assert 'all' not in tests
270 assert 'help' not in tests
271
272 CWD = os.getcwd()
273 apt_conf_template = """
274 {
275   // Location of the state dir
276   State "var/lib/apt/"
277   {
278      Lists "lists/";
279      xstatus "xstatus";
280      userstatus "status.user";
281      cdroms "cdroms.list";
282   };
283
284   // Location of the cache dir
285   Cache "var/cache/apt/" {
286      Archives "archives/";
287      srcpkgcache "srcpkgcache.bin";
288      pkgcache "pkgcache.bin";
289   };
290
291   // Config files
292   Etc "etc/apt/" {
293      SourceList "sources.list";
294      Main "apt.conf";
295      Preferences "preferences";
296      Parts "apt.conf.d/";
297   };
298
299   // Locations of binaries
300   Bin {
301      methods "/usr/lib/apt/methods/";
302      gzip "/bin/gzip";
303      gpg  "/usr/bin/gpgv";
304      dpkg "/usr/bin/dpkg --simulate";
305      dpkg-source "/usr/bin/dpkg-source";
306      dpkg-buildpackage "/usr/bin/dpkg-buildpackage";
307      apt-get "/usr/bin/apt-get";
308      apt-cache "/usr/bin/apt-cache";
309   };
310 };
311
312 /* Options you can set to see some debugging text They correspond to names
313    of classes in the source code */
314 Debug
315 {
316   pkgProblemResolver "false";
317   pkgDepCache::AutoInstall "false"; // what packages apt install to satify dependencies
318   pkgAcquire "false";
319   pkgAcquire::Worker "false";
320   pkgAcquire::Auth "false";
321   pkgDPkgPM "false";
322   pkgDPkgProgressReporting "false";
323   pkgOrderList "false";
324   BuildDeps "false";
325
326   pkgInitialize "false";   // This one will dump the configuration space
327   NoLocking "false";
328   Acquire::Ftp "false";    // Show ftp command traffic
329   Acquire::Http "false";   // Show http command traffic
330   Acquire::gpgv "false";   // Show the gpgv traffic
331   aptcdrom "false";        // Show found package files
332   IdentCdrom "false";
333
334 }
335 """
336 apt_p2p_conf_template = """
337 [DEFAULT]
338
339 # Port to listen on for all requests (TCP and UDP)
340 PORT = %(PORT)s
341     
342 # The rate to limit sending data to peers to, in KBytes/sec.
343 # Set this to 0 to not limit the upload bandwidth.
344 UPLOAD_LIMIT = 100
345
346 # Directory to store the downloaded files in
347 CACHE_DIR = %(CACHE_DIR)s
348     
349 # Other directories containing packages to share with others
350 # WARNING: all files in these directories will be hashed and available
351 #          for everybody to download
352 # OTHER_DIRS = 
353     
354 # User name to try and run as
355 # USERNAME = 
356
357 # Whether it's OK to use an IP addres from a known local/private range
358 LOCAL_OK = yes
359
360 # Unload the packages cache after an interval of inactivity this long.
361 # The packages cache uses a lot of memory, and only takes a few seconds
362 # to reload when a new request arrives.
363 UNLOAD_PACKAGES_CACHE = 5m
364
365 # Refresh the DHT keys after this much time has passed.
366 # This should be a time slightly less than the DHT's KEY_EXPIRE value.
367 KEY_REFRESH = 57m
368
369 # Which DHT implementation to use.
370 # It must be possile to do "from <DHT>.DHT import DHT" to get a class that
371 # implements the IDHT interface.
372 DHT = apt_p2p_Khashmir
373
374 # Whether to only run the DHT (for providing only a bootstrap node)
375 DHT-ONLY = %(DHT-ONLY)s
376
377 [apt_p2p_Khashmir]
378 # bootstrap nodes to contact to join the DHT
379 BOOTSTRAP = %(BOOTSTRAP)s
380
381 # whether this node is a bootstrap node
382 BOOTSTRAP_NODE = %(BOOTSTRAP_NODE)s
383
384 # Kademlia "K" constant, this should be an even number
385 K = 8
386
387 # SHA1 is 160 bits long
388 HASH_LENGTH = 160
389
390 # checkpoint every this many seconds
391 CHECKPOINT_INTERVAL = 5m
392
393 # concurrent xmlrpc calls per find node/value request!
394 CONCURRENT_REQS = 4
395
396 # how many hosts to post to
397 STORE_REDUNDANCY = 3
398
399 # How many values to attempt to retrieve from the DHT.
400 # Setting this to 0 will try and get all values (which could take a while if
401 # a lot of nodes have values). Setting it negative will try to get that
402 # number of results from only the closest STORE_REDUNDANCY nodes to the hash.
403 # The default is a large negative number so all values from the closest
404 # STORE_REDUNDANCY nodes will be retrieved.
405 RETRIEVE_VALUES = -10000
406
407 # how many times in a row a node can fail to respond before it's booted from the routing table
408 MAX_FAILURES = 3
409
410 # never ping a node more often than this
411 MIN_PING_INTERVAL = 15m
412
413 # refresh buckets that haven't been touched in this long
414 BUCKET_STALENESS = 1h
415
416 # expire entries older than this
417 KEY_EXPIRE = 1h
418
419 # whether to spew info about the requests/responses in the protocol
420 SPEW = yes
421 """
422
423 def rmrf(top):
424     """Remove all the files and directories below a top-level one.
425     
426     @type top: C{string}
427     @param top: the top-level directory to start at
428     
429     """
430     
431     for root, dirs, files in os.walk(top, topdown=False):
432         for name in files:
433             os.remove(os.path.join(root, name))
434         for name in dirs:
435             os.rmdir(os.path.join(root, name))
436
437 def join(dir):
438     """Join together a list of directories into a path string.
439     
440     @type dir: C{list} of C{string}
441     @param dir: the path to join together
442     @rtype: C{string}
443     @return: the joined together path
444     
445     """
446     
447     joined = ''
448     for i in dir:
449         joined = os.path.join(joined, i)
450     return joined
451
452 def makedirs(dir):
453     """Create all the directories to make a path.
454     
455     @type dir: C{list} of C{string}
456     @param dir: the path to create
457     
458     """
459     if not os.path.exists(join(dir)):
460         os.makedirs(join(dir))
461
462 def touch(path):
463     """Create an empty file.
464     
465     @type path: C{list} of C{string}
466     @param path: the path to create
467     
468     """
469     
470     f = open(join(path), 'w')
471     f.close()
472
473 def start(cmd, args, work_dir = None):
474     """Fork and start a background process running.
475     
476     @type cmd: C{string}
477     @param cmd: the name of the command to run
478     @type args: C{list} of C{string}
479     @param args: the argument to pass to the command
480     @type work_dir: C{string}
481     @param work_dir: the directory to change to to execute the child process in
482         (optional, defaults to the current directory)
483     @rtype: C{int}
484     @return: the PID of the forked process
485     
486     """
487     
488     new_cmd = [cmd] + args
489     pid = os.spawnvp(os.P_NOWAIT, new_cmd[0], new_cmd)
490     return pid
491
492 def stop(pid):
493     """Stop a forked background process that is running.
494     
495     @type pid: C{int}
496     @param pid: the PID of the process to stop
497     @rtype: C{int}
498     @return: the return status code from the child
499     
500     """
501
502     # First try a keyboard interrupt
503     os.kill(pid, signal.SIGINT)
504     for i in xrange(5):
505         sleep(1)
506         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
507         if r_pid:
508             return r_value
509     
510     # Try a keyboard interrupt again, just in case
511     os.kill(pid, signal.SIGINT)
512     for i in xrange(5):
513         sleep(1)
514         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
515         if r_pid:
516             return r_value
517
518     # Try a terminate
519     os.kill(pid, signal.SIGTERM)
520     for i in xrange(5):
521         sleep(1)
522         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
523         if r_pid:
524             return r_value
525
526     # Finally a kill, don't return until killed
527     os.kill(pid, signal.SIGKILL)
528     while not r_pid:
529         sleep(1)
530         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
531
532     return r_value
533
534 def apt_get(num_down, cmd):
535     """Start an apt-get process in the background.
536
537     The default argument specified to the apt-get invocation are
538     'apt-get -d -q -c <conf_file>'. Any additional arguments (including
539     the apt-get action to use) should be specified.
540     
541     @type num_down: C{int}
542     @param num_down: the number of the downloader to use
543     @type cmd: C{list} of C{string}
544     @param cmd: the arguments to pass to the apt-get process
545     @rtype: C{int}
546     @return: the PID of the background process
547     
548     """
549     
550     print '*************** apt-get (' + str(num_down) + ') ' + ' '.join(cmd) + ' ****************'
551     apt_conf = join([down_dir(num_down), 'etc', 'apt', 'apt.conf'])
552     dpkg_status = join([down_dir(num_down), 'var', 'lib', 'dpkg', 'status'])
553     args = ['-d', '-c', apt_conf, '-o', 'Dir::state::status='+dpkg_status] + cmd
554     pid = start('apt-get', args)
555     return pid
556
557 def bootstrap_address(num_boot):
558     """Determine the bootstrap address to use for a node.
559     
560     @type num_boot: C{int}
561     @param num_boot: the number of the bootstrap node
562     @rtype: C{string}
563     @return: the bootstrap address to use
564     
565     """
566     
567     return 'localhost:1' + str(num_boot) + '969'
568
569 def down_dir(num_down):
570     """Determine the working directory to use for a downloader.
571     
572     @type num_down: C{int}
573     @param num_down: the number of the downloader
574     @rtype: C{string}
575     @return: the downloader's directory
576     
577     """
578     
579     return os.path.join(CWD,'downloader' + str(num_down))
580
581 def boot_dir(num_boot):
582     """Determine the working directory to use for a bootstrap node.
583     
584     @type num_boot: C{int}
585     @param num_boot: the number of the bootstrap node
586     @rtype: C{string}
587     @return: the bootstrap node's directory
588     
589     """
590     
591     return os.path.join(CWD,'bootstrap' + str(num_boot))
592
593 def start_downloader(bootstrap_addresses, num_down, options = {},
594                      mirror = 'ftp.us.debian.org/debian', 
595                      suites = 'main contrib non-free', clean = True):
596     """Initialize a new downloader process.
597
598     The default arguments specified to the downloader invocation are
599     the configuration directory, apt port, minport, maxport and the
600     maximum upload rate. 
601     Any additional arguments needed should be specified by L{options}.
602     
603     @type num_down: C{int}
604     @param num_down: the number of the downloader to use
605     @type options: C{dictionary}
606     @param options: the dictionary of string formatting values for creating
607         the apt-p2p configuration file (see L{apt_p2p_conf_template} above).
608         (optional, defaults to only using the default arguments)
609     @type mirror: C{string}
610     @param mirror: the Debian mirror to use
611         (optional, defaults to 'ftp.us.debian.org/debian')
612     @type suites: C{string}
613     @param suites: space separated list of suites to download
614         (optional, defaults to 'main contrib non-free')
615     @type clean: C{boolean}
616     @param clean: whether to remove any previous downloader files
617         (optional, defaults to removing them)
618     @rtype: C{int}
619     @return: the PID of the downloader process
620     
621     """
622     
623     assert num_down < 100
624     
625     print '************************** Starting Downloader ' + str(num_down) + ' **************************'
626
627     downloader_dir = down_dir(num_down)
628     
629     if clean:
630         try:
631             rmrf(downloader_dir)
632         except:
633             pass
634     
635         # Create the directory structure needed by apt
636         makedirs([downloader_dir, 'etc', 'apt', 'apt.conf.d'])
637         makedirs([downloader_dir, 'var', 'lib', 'apt', 'lists', 'partial'])
638         makedirs([downloader_dir, 'var', 'lib', 'dpkg'])
639         makedirs([downloader_dir, 'var', 'cache', 'apt', 'archives', 'partial'])
640         touch([downloader_dir, 'var', 'lib', 'apt', 'lists', 'lock'])
641         touch([downloader_dir, 'var', 'lib', 'dpkg', 'lock'])
642         touch([downloader_dir, 'var', 'lib', 'dpkg', 'status'])
643         touch([downloader_dir, 'var', 'cache', 'apt', 'archives', 'lock'])
644
645         # Create apt's config files
646         f = open(join([downloader_dir, 'etc', 'apt', 'sources.list']), 'w')
647         f.write('deb http://localhost:1%02d77/%s/ unstable %s\n' % (num_down, mirror, suites))
648         f.close()
649
650         f = open(join([downloader_dir, 'etc', 'apt', 'apt.conf']), 'w')
651         f.write('Dir "' + downloader_dir + '"')
652         f.write(apt_conf_template)
653         f.close()
654
655     defaults = {'PORT': '1%02d77' % num_down,
656                 'CACHE_DIR': downloader_dir,
657                 'DHT-ONLY': 'no',
658                 'BOOTSTRAP': bootstrap_addresses,
659                 'BOOTSTRAP_NODE': 'no'}
660
661     for k in options:
662         defaults[k] = options[k]
663     f = open(join([downloader_dir, 'apt-p2p.conf']), 'w')
664     f.write(apt_p2p_conf_template % defaults)
665     f.close()
666     
667     pid = start('python', [join([sys.path[0], 'apt-p2p.py']),
668                            '--config-file=' + join([downloader_dir, 'apt-p2p.conf']),
669                            '--log-file=' + join([downloader_dir, 'apt-p2p.log']),],
670                 downloader_dir)
671     return pid
672
673 def start_bootstrap(bootstrap_addresses, num_boot, options = [], clean = True):
674     """Initialize a new bootstrap node process.
675
676     The default arguments specified to the apt-p2p invocation are
677     the state file and port to use. Any additional arguments needed 
678     should be specified by L{options}.
679     
680     @type num_boot: C{int}
681     @param num_boot: the number of the bootstrap node to use
682     @type options: C{list} of C{string}
683     @param options: the arguments to pass to the bootstrap node
684         (optional, defaults to only using the default arguments)
685     @type clean: C{boolean}
686     @param clean: whether to remove any previous bootstrap node files
687         (optional, defaults to removing them)
688     @rtype: C{int}
689     @return: the PID of the downloader process
690     
691     """
692     
693     assert num_boot < 10
694
695     print '************************** Starting Bootstrap ' + str(num_boot) + ' **************************'
696
697     bootstrap_dir = boot_dir(num_boot)
698     
699     if clean:
700         try:
701             rmrf(bootstrap_dir)
702         except:
703             pass
704
705     makedirs([bootstrap_dir])
706
707     defaults = {'PORT': '1%d969' % num_boot,
708                 'CACHE_DIR': bootstrap_dir,
709                 'DHT-ONLY': 'yes',
710                 'BOOTSTRAP': bootstrap_addresses,
711                 'BOOTSTRAP_NODE': 'yes'}
712
713     for k in options:
714         defaults[k] = options[k]
715     f = open(join([bootstrap_dir, 'apt-p2p.conf']), 'w')
716     f.write(apt_p2p_conf_template % defaults)
717     f.close()
718     
719     pid = start('python', [join([sys.path[0], 'apt-p2p.py']),
720                            '--config-file=' + join([bootstrap_dir, 'apt-p2p.conf']),
721                            '--log-file=' + join([bootstrap_dir, 'apt-p2p.log']),],
722                 bootstrap_dir)
723
724     return pid
725
726 def run_test(bootstraps, downloaders, apt_get_queue):
727     """Run a single test.
728     
729     @type bootstraps: C{dictionary} of {C{int}: C{list} of C{string}}
730     @param bootstraps: the bootstrap nodes to start, keys are the bootstrap numbers and
731         values are the list of options to invoke the bootstrap node with
732     @type downloaders: C{dictionary} of {C{int}: (C{int}, C{list} of C{string})}
733     @param downloaders: the downloaders to start, keys are the downloader numbers and
734         values are the list of options to invoke the downloader with
735     @type apt_get_queue: C{list} of (C{int}, C{list} of C{string})
736     @param apt_get_queue: the apt-get downloader to use and commands to execute
737     @rtype: C{list} of (C{float}, C{int})
738     @return: the execution time and returned status code for each element of apt_get_queue
739     
740     """
741     
742     running_bootstraps = {}
743     running_downloaders = {}
744     running_apt_get = {}
745     apt_get_results = []
746
747     try:
748         boot_keys = bootstraps.keys()
749         boot_keys.sort()
750         bootstrap_addresses = bootstrap_address(boot_keys[0])
751         for i in xrange(1, len(boot_keys)):
752             bootstrap_addresses += '\n      ' + bootstrap_address(boot_keys[i])
753             
754         for k, v in bootstraps.items():
755             running_bootstraps[k] = start_bootstrap(bootstrap_addresses, k, **v)
756         
757         sleep(5)
758         
759         for k, v in downloaders.items():
760             running_downloaders[k] = start_downloader(bootstrap_addresses, k, **v)
761     
762         sleep(5)
763         
764         for (num_down, cmd) in apt_get_queue:
765             running_apt_get[num_down] = apt_get(num_down, cmd)
766             start_time = time()
767             (pid, r_value) = os.waitpid(running_apt_get[num_down], 0)
768             elapsed = time() - start_time
769             del running_apt_get[num_down]
770             r_value = r_value / 256
771             apt_get_results.append((elapsed, r_value))
772
773             if r_value == 0:
774                 print '********** apt-get completed successfully in ' +  str(elapsed) + ' sec. *****************'
775             else:
776                 print '********** apt-get finished with status ' + str(r_value) + ' in ' +  str(elapsed) + ' sec. ************'
777         
778             sleep(5)
779             
780     except:
781         print '************************** Exception occurred **************************'
782         print_exc()
783         print '************************** will attempt to shut down *******************'
784         
785     print '*********************** shutting down the apt-gets *******************'
786     for k, v in running_apt_get.items():
787         try:
788             print 'apt-get', k, stop(v)
789         except:
790             print '************************** Exception occurred **************************'
791             print_exc()
792
793     sleep(5)
794
795     print '*********************** shutting down the downloaders *******************'
796     for k, v in running_downloaders.items():
797         try:
798             print 'downloader', k, stop(v)
799         except:
800             print '************************** Exception occurred **************************'
801             print_exc()
802
803     sleep(5)
804
805     print '************************** shutting down the bootstraps *******************'
806     for k, v in running_bootstraps.items():
807         try:
808             print 'bootstrap', k, stop(v)
809         except:
810             print '************************** Exception occurred **************************'
811             print_exc()
812
813     print '************************** Test Results *******************'
814     i = -1
815     for (num_down, cmd) in apt_get_queue:
816         i += 1
817         s = str(num_down) + ': "apt-get ' + ' '.join(cmd) + '" '
818         if len(apt_get_results) > i:
819             (elapsed, r_value) = apt_get_results[i]
820             s += 'took ' + str(elapsed) + ' secs (' + str(r_value) + ')'
821         else:
822             s += 'did not complete'
823         print s
824     
825     return apt_get_results
826
827 def get_usage():
828     """Get the usage information to display to the user.
829     
830     @rtype: C{string}
831     @return: the usage information to display
832     
833     """
834     
835     s = 'Usage: ' + sys.argv[0] + ' (all|<test>|help)\n\n'
836     s += '  all    - run all the tests\n'
837     s += '  help   - display this usage information\n'
838     s += '  <test> - run the <test> test (see list below for valid tests)\n\n'
839     
840     t = tests.items()
841     t.sort()
842     for k, v in t:
843         s += 'test "' + str(k) + '" - ' + v[0] + '\n'
844     
845     return s
846
847 if __name__ == '__main__':
848     if len(sys.argv) != 2:
849         print get_usage()
850     elif sys.argv[1] == 'all':
851         for k, v in tests.items():
852             run_test(v[1], v[2], v[3])
853     elif sys.argv[1] in tests:
854         v = tests[sys.argv[1]]
855         run_test(v[1], v[2], v[3])
856     elif sys.argv[1] == 'help':
857         print get_usage()
858     else:
859         print 'Unknown test to run:', sys.argv[1], '\n'
860         print get_usage()
861