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