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