Clean up the DHT config, making K and HASH_LENGTH constants instead.
[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 # The minimum number of peers before the mirror is not used.
347 # If there are fewer peers than this for a file, the mirror will also be
348 # used to speed up the download. Set to 0 to never use the mirror if
349 # there are peers.
350 MIN_DOWNLOAD_PEERS = 3
351
352 # Directory to store the downloaded files in
353 CACHE_DIR = %(CACHE_DIR)s
354     
355 # Other directories containing packages to share with others
356 # WARNING: all files in these directories will be hashed and available
357 #          for everybody to download
358 # OTHER_DIRS = 
359     
360 # Whether it's OK to use an IP addres from a known local/private range
361 LOCAL_OK = yes
362
363 # Unload the packages cache after an interval of inactivity this long.
364 # The packages cache uses a lot of memory, and only takes a few seconds
365 # to reload when a new request arrives.
366 UNLOAD_PACKAGES_CACHE = 5m
367
368 # Refresh the DHT keys after this much time has passed.
369 # This should be a time slightly less than the DHT's KEY_EXPIRE value.
370 KEY_REFRESH = 57m
371
372 # Which DHT implementation to use.
373 # It must be possile to do "from <DHT>.DHT import DHT" to get a class that
374 # implements the IDHT interface.
375 DHT = apt_p2p_Khashmir
376
377 # Whether to only run the DHT (for providing only a bootstrap node)
378 DHT-ONLY = %(DHT-ONLY)s
379
380 [apt_p2p_Khashmir]
381 # bootstrap nodes to contact to join the DHT
382 BOOTSTRAP = %(BOOTSTRAP)s
383
384 # whether this node is a bootstrap node
385 BOOTSTRAP_NODE = %(BOOTSTRAP_NODE)s
386
387 # checkpoint every this many seconds
388 CHECKPOINT_INTERVAL = 5m
389
390 # concurrent xmlrpc calls per find node/value request!
391 CONCURRENT_REQS = 4
392
393 # how many hosts to post to
394 STORE_REDUNDANCY = 3
395
396 # How many values to attempt to retrieve from the DHT.
397 # Setting this to 0 will try and get all values (which could take a while if
398 # a lot of nodes have values). Setting it negative will try to get that
399 # number of results from only the closest STORE_REDUNDANCY nodes to the hash.
400 # The default is a large negative number so all values from the closest
401 # STORE_REDUNDANCY nodes will be retrieved.
402 RETRIEVE_VALUES = -10000
403
404 # how many times in a row a node can fail to respond before it's booted from the routing table
405 MAX_FAILURES = 3
406
407 # never ping a node more often than this
408 MIN_PING_INTERVAL = 15m
409
410 # refresh buckets that haven't been touched in this long
411 BUCKET_STALENESS = 1h
412
413 # expire entries older than this
414 KEY_EXPIRE = 1h
415
416 # whether to spew info about the requests/responses in the protocol
417 SPEW = yes
418 """
419
420 def rmrf(top):
421     """Remove all the files and directories below a top-level one.
422     
423     @type top: C{string}
424     @param top: the top-level directory to start at
425     
426     """
427     
428     for root, dirs, files in os.walk(top, topdown=False):
429         for name in files:
430             os.remove(os.path.join(root, name))
431         for name in dirs:
432             os.rmdir(os.path.join(root, name))
433
434 def join(dir):
435     """Join together a list of directories into a path string.
436     
437     @type dir: C{list} of C{string}
438     @param dir: the path to join together
439     @rtype: C{string}
440     @return: the joined together path
441     
442     """
443     
444     joined = ''
445     for i in dir:
446         joined = os.path.join(joined, i)
447     return joined
448
449 def makedirs(dir):
450     """Create all the directories to make a path.
451     
452     @type dir: C{list} of C{string}
453     @param dir: the path to create
454     
455     """
456     if not os.path.exists(join(dir)):
457         os.makedirs(join(dir))
458
459 def touch(path):
460     """Create an empty file.
461     
462     @type path: C{list} of C{string}
463     @param path: the path to create
464     
465     """
466     
467     f = open(join(path), 'w')
468     f.close()
469
470 def start(cmd, args, work_dir = None):
471     """Fork and start a background process running.
472     
473     @type cmd: C{string}
474     @param cmd: the name of the command to run
475     @type args: C{list} of C{string}
476     @param args: the argument to pass to the command
477     @type work_dir: C{string}
478     @param work_dir: the directory to change to to execute the child process in
479         (optional, defaults to the current directory)
480     @rtype: C{int}
481     @return: the PID of the forked process
482     
483     """
484     
485     new_cmd = [cmd] + args
486     pid = os.spawnvp(os.P_NOWAIT, new_cmd[0], new_cmd)
487     return pid
488
489 def stop(pid):
490     """Stop a forked background process that is running.
491     
492     @type pid: C{int}
493     @param pid: the PID of the process to stop
494     @rtype: C{int}
495     @return: the return status code from the child
496     
497     """
498
499     # First try a keyboard interrupt
500     os.kill(pid, signal.SIGINT)
501     for i in xrange(5):
502         sleep(1)
503         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
504         if r_pid:
505             return r_value
506     
507     # Try a keyboard interrupt again, just in case
508     os.kill(pid, signal.SIGINT)
509     for i in xrange(5):
510         sleep(1)
511         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
512         if r_pid:
513             return r_value
514
515     # Try a terminate
516     os.kill(pid, signal.SIGTERM)
517     for i in xrange(5):
518         sleep(1)
519         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
520         if r_pid:
521             return r_value
522
523     # Finally a kill, don't return until killed
524     os.kill(pid, signal.SIGKILL)
525     while not r_pid:
526         sleep(1)
527         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
528
529     return r_value
530
531 def apt_get(num_down, cmd):
532     """Start an apt-get process in the background.
533
534     The default argument specified to the apt-get invocation are
535     'apt-get -d -q -c <conf_file>'. Any additional arguments (including
536     the apt-get action to use) should be specified.
537     
538     @type num_down: C{int}
539     @param num_down: the number of the downloader to use
540     @type cmd: C{list} of C{string}
541     @param cmd: the arguments to pass to the apt-get process
542     @rtype: C{int}
543     @return: the PID of the background process
544     
545     """
546     
547     print '*************** apt-get (' + str(num_down) + ') ' + ' '.join(cmd) + ' ****************'
548     apt_conf = join([down_dir(num_down), 'etc', 'apt', 'apt.conf'])
549     dpkg_status = join([down_dir(num_down), 'var', 'lib', 'dpkg', 'status'])
550     args = ['-d', '-c', apt_conf, '-o', 'Dir::state::status='+dpkg_status] + cmd
551     pid = start('apt-get', args)
552     return pid
553
554 def bootstrap_address(num_boot):
555     """Determine the bootstrap address to use for a node.
556     
557     @type num_boot: C{int}
558     @param num_boot: the number of the bootstrap node
559     @rtype: C{string}
560     @return: the bootstrap address to use
561     
562     """
563     
564     return 'localhost:1' + str(num_boot) + '969'
565
566 def down_dir(num_down):
567     """Determine the working directory to use for a downloader.
568     
569     @type num_down: C{int}
570     @param num_down: the number of the downloader
571     @rtype: C{string}
572     @return: the downloader's directory
573     
574     """
575     
576     return os.path.join(CWD,'downloader' + str(num_down))
577
578 def boot_dir(num_boot):
579     """Determine the working directory to use for a bootstrap node.
580     
581     @type num_boot: C{int}
582     @param num_boot: the number of the bootstrap node
583     @rtype: C{string}
584     @return: the bootstrap node's directory
585     
586     """
587     
588     return os.path.join(CWD,'bootstrap' + str(num_boot))
589
590 def start_downloader(bootstrap_addresses, num_down, options = {},
591                      mirror = 'ftp.us.debian.org/debian', 
592                      suites = 'main contrib non-free', clean = True):
593     """Initialize a new downloader process.
594
595     The default arguments specified to the downloader invocation are
596     the configuration directory, apt port, minport, maxport and the
597     maximum upload rate. 
598     Any additional arguments needed should be specified by L{options}.
599     
600     @type num_down: C{int}
601     @param num_down: the number of the downloader to use
602     @type options: C{dictionary}
603     @param options: the dictionary of string formatting values for creating
604         the apt-p2p configuration file (see L{apt_p2p_conf_template} above).
605         (optional, defaults to only using the default arguments)
606     @type mirror: C{string}
607     @param mirror: the Debian mirror to use
608         (optional, defaults to 'ftp.us.debian.org/debian')
609     @type suites: C{string}
610     @param suites: space separated list of suites to download
611         (optional, defaults to 'main contrib non-free')
612     @type clean: C{boolean}
613     @param clean: whether to remove any previous downloader files
614         (optional, defaults to removing them)
615     @rtype: C{int}
616     @return: the PID of the downloader process
617     
618     """
619     
620     assert num_down < 100
621     
622     print '************************** Starting Downloader ' + str(num_down) + ' **************************'
623
624     downloader_dir = down_dir(num_down)
625     
626     if clean:
627         try:
628             rmrf(downloader_dir)
629         except:
630             pass
631     
632         # Create the directory structure needed by apt
633         makedirs([downloader_dir, 'etc', 'apt', 'apt.conf.d'])
634         makedirs([downloader_dir, 'var', 'lib', 'apt', 'lists', 'partial'])
635         makedirs([downloader_dir, 'var', 'lib', 'dpkg'])
636         makedirs([downloader_dir, 'var', 'cache', 'apt', 'archives', 'partial'])
637         touch([downloader_dir, 'var', 'lib', 'apt', 'lists', 'lock'])
638         touch([downloader_dir, 'var', 'lib', 'dpkg', 'lock'])
639         touch([downloader_dir, 'var', 'lib', 'dpkg', 'status'])
640         touch([downloader_dir, 'var', 'cache', 'apt', 'archives', 'lock'])
641
642         # Create apt's config files
643         f = open(join([downloader_dir, 'etc', 'apt', 'sources.list']), 'w')
644         f.write('deb http://localhost:1%02d77/%s/ unstable %s\n' % (num_down, mirror, suites))
645         f.close()
646
647         f = open(join([downloader_dir, 'etc', 'apt', 'apt.conf']), 'w')
648         f.write('Dir "' + downloader_dir + '"')
649         f.write(apt_conf_template)
650         f.close()
651
652     defaults = {'PORT': '1%02d77' % num_down,
653                 'CACHE_DIR': downloader_dir,
654                 'DHT-ONLY': 'no',
655                 'BOOTSTRAP': bootstrap_addresses,
656                 'BOOTSTRAP_NODE': 'no'}
657
658     for k in options:
659         defaults[k] = options[k]
660     f = open(join([downloader_dir, 'apt-p2p.conf']), 'w')
661     f.write(apt_p2p_conf_template % defaults)
662     f.close()
663     
664     pid = start('python', [join([sys.path[0], 'apt-p2p.py']),
665                            '--config-file=' + join([downloader_dir, 'apt-p2p.conf']),
666                            '--log-file=' + join([downloader_dir, 'apt-p2p.log']),],
667                 downloader_dir)
668     return pid
669
670 def start_bootstrap(bootstrap_addresses, num_boot, options = [], clean = True):
671     """Initialize a new bootstrap node process.
672
673     The default arguments specified to the apt-p2p invocation are
674     the state file and port to use. Any additional arguments needed 
675     should be specified by L{options}.
676     
677     @type num_boot: C{int}
678     @param num_boot: the number of the bootstrap node to use
679     @type options: C{list} of C{string}
680     @param options: the arguments to pass to the bootstrap node
681         (optional, defaults to only using the default arguments)
682     @type clean: C{boolean}
683     @param clean: whether to remove any previous bootstrap node files
684         (optional, defaults to removing them)
685     @rtype: C{int}
686     @return: the PID of the downloader process
687     
688     """
689     
690     assert num_boot < 10
691
692     print '************************** Starting Bootstrap ' + str(num_boot) + ' **************************'
693
694     bootstrap_dir = boot_dir(num_boot)
695     
696     if clean:
697         try:
698             rmrf(bootstrap_dir)
699         except:
700             pass
701
702     makedirs([bootstrap_dir])
703
704     defaults = {'PORT': '1%d969' % num_boot,
705                 'CACHE_DIR': bootstrap_dir,
706                 'DHT-ONLY': 'yes',
707                 'BOOTSTRAP': bootstrap_addresses,
708                 'BOOTSTRAP_NODE': 'yes'}
709
710     for k in options:
711         defaults[k] = options[k]
712     f = open(join([bootstrap_dir, 'apt-p2p.conf']), 'w')
713     f.write(apt_p2p_conf_template % defaults)
714     f.close()
715     
716     pid = start('python', [join([sys.path[0], 'apt-p2p.py']),
717                            '--config-file=' + join([bootstrap_dir, 'apt-p2p.conf']),
718                            '--log-file=' + join([bootstrap_dir, 'apt-p2p.log']),],
719                 bootstrap_dir)
720
721     return pid
722
723 def run_test(bootstraps, downloaders, apt_get_queue):
724     """Run a single test.
725     
726     @type bootstraps: C{dictionary} of {C{int}: C{list} of C{string}}
727     @param bootstraps: the bootstrap nodes to start, keys are the bootstrap numbers and
728         values are the list of options to invoke the bootstrap node with
729     @type downloaders: C{dictionary} of {C{int}: (C{int}, C{list} of C{string})}
730     @param downloaders: the downloaders to start, keys are the downloader numbers and
731         values are the list of options to invoke the downloader with
732     @type apt_get_queue: C{list} of (C{int}, C{list} of C{string})
733     @param apt_get_queue: the apt-get downloader to use and commands to execute
734     @rtype: C{list} of (C{float}, C{int})
735     @return: the execution time and returned status code for each element of apt_get_queue
736     
737     """
738     
739     running_bootstraps = {}
740     running_downloaders = {}
741     running_apt_get = {}
742     apt_get_results = []
743
744     try:
745         boot_keys = bootstraps.keys()
746         boot_keys.sort()
747         bootstrap_addresses = bootstrap_address(boot_keys[0])
748         for i in xrange(1, len(boot_keys)):
749             bootstrap_addresses += '\n      ' + bootstrap_address(boot_keys[i])
750             
751         for k, v in bootstraps.items():
752             running_bootstraps[k] = start_bootstrap(bootstrap_addresses, k, **v)
753         
754         sleep(5)
755         
756         for k, v in downloaders.items():
757             running_downloaders[k] = start_downloader(bootstrap_addresses, k, **v)
758     
759         sleep(5)
760         
761         for (num_down, cmd) in apt_get_queue:
762             running_apt_get[num_down] = apt_get(num_down, cmd)
763             start_time = time()
764             (pid, r_value) = os.waitpid(running_apt_get[num_down], 0)
765             elapsed = time() - start_time
766             del running_apt_get[num_down]
767             r_value = r_value / 256
768             apt_get_results.append((elapsed, r_value))
769
770             if r_value == 0:
771                 print '********** apt-get completed successfully in ' +  str(elapsed) + ' sec. *****************'
772             else:
773                 print '********** apt-get finished with status ' + str(r_value) + ' in ' +  str(elapsed) + ' sec. ************'
774         
775             sleep(5)
776             
777     except:
778         print '************************** Exception occurred **************************'
779         print_exc()
780         print '************************** will attempt to shut down *******************'
781         
782     print '*********************** shutting down the apt-gets *******************'
783     for k, v in running_apt_get.items():
784         try:
785             print 'apt-get', k, stop(v)
786         except:
787             print '************************** Exception occurred **************************'
788             print_exc()
789
790     sleep(5)
791
792     print '*********************** shutting down the downloaders *******************'
793     for k, v in running_downloaders.items():
794         try:
795             print 'downloader', k, stop(v)
796         except:
797             print '************************** Exception occurred **************************'
798             print_exc()
799
800     sleep(5)
801
802     print '************************** shutting down the bootstraps *******************'
803     for k, v in running_bootstraps.items():
804         try:
805             print 'bootstrap', k, stop(v)
806         except:
807             print '************************** Exception occurred **************************'
808             print_exc()
809
810     print '************************** Test Results *******************'
811     i = -1
812     for (num_down, cmd) in apt_get_queue:
813         i += 1
814         s = str(num_down) + ': "apt-get ' + ' '.join(cmd) + '" '
815         if len(apt_get_results) > i:
816             (elapsed, r_value) = apt_get_results[i]
817             s += 'took ' + str(elapsed) + ' secs (' + str(r_value) + ')'
818         else:
819             s += 'did not complete'
820         print s
821     
822     return apt_get_results
823
824 def get_usage():
825     """Get the usage information to display to the user.
826     
827     @rtype: C{string}
828     @return: the usage information to display
829     
830     """
831     
832     s = 'Usage: ' + sys.argv[0] + ' (all|<test>|help)\n\n'
833     s += '  all    - run all the tests\n'
834     s += '  help   - display this usage information\n'
835     s += '  <test> - run the <test> test (see list below for valid tests)\n\n'
836     
837     t = tests.items()
838     t.sort()
839     for k, v in t:
840         s += 'test "' + str(k) + '" - ' + v[0] + '\n'
841     
842     return s
843
844 if __name__ == '__main__':
845     if len(sys.argv) != 2:
846         print get_usage()
847     elif sys.argv[1] == 'all':
848         for k, v in tests.items():
849             run_test(v[1], v[2], v[3])
850     elif sys.argv[1] in tests:
851         v = tests[sys.argv[1]]
852         run_test(v[1], v[2], v[3])
853     elif sys.argv[1] == 'help':
854         print get_usage()
855     else:
856         print 'Unknown test to run:', sys.argv[1], '\n'
857         print get_usage()
858