Fix an error in the actions that allowed for the result to be sent twice.
[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         'a': ('Test pipelining and caching, can also interrupt or restart to test resuming.',
268              {1: {'clean': False}},
269              {1: {'clean': False},
270               2: {'clean': False}},
271             [(1, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
272                    'aspectj-doc', 'fop-doc', 'asis-doc',
273                    'bison-doc', 'crash-whitepaper',
274                    'bash-doc', 'apt-howto-common', 'autotools-dev',
275                    'aptitude-doc-en', 'asr-manpages',
276                    'atomix-data', 'alcovebook-sgml-doc',
277                    'afbackup-common', 'airstrike-common',
278                    ]),
279               (1, ['update']), 
280               (1, ['update']), 
281               (1, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
282                    'aspectj-doc', 'fop-doc', 'asis-doc',
283                    'bison-doc', 'crash-whitepaper',
284                    'bash-doc', 'apt-howto-common', 'autotools-dev',
285                    'aptitude-doc-en', 'asr-manpages',
286                    'atomix-data', 'alcovebook-sgml-doc',
287                    'afbackup-common', 'airstrike-common',
288                    ]),
289               (1, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
290                    'aspectj-doc', 'fop-doc', 'asis-doc',
291                    'bison-doc', 'crash-whitepaper',
292                    'bash-doc', 'apt-howto-common', 'autotools-dev',
293                    'aptitude-doc-en', 'asr-manpages',
294                    'atomix-data', 'alcovebook-sgml-doc',
295                    'afbackup-common', 'airstrike-common',
296                    ]),
297               (2, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
298                    'aspectj-doc', 'fop-doc', 'asis-doc',
299                    'bison-doc', 'crash-whitepaper',
300                    'bash-doc', 'apt-howto-common', 'autotools-dev',
301                    'aptitude-doc-en', 'asr-manpages',
302                    'atomix-data', 'alcovebook-sgml-doc',
303                    'afbackup-common', 'airstrike-common',
304                    ]),
305               (2, ['update']), 
306               (2, ['update']), 
307               (2, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
308                    'aspectj-doc', 'fop-doc', 'asis-doc',
309                    'bison-doc', 'crash-whitepaper',
310                    'bash-doc', 'apt-howto-common', 'autotools-dev',
311                    'aptitude-doc-en', 'asr-manpages',
312                    'atomix-data', 'alcovebook-sgml-doc',
313                    'afbackup-common', 'airstrike-common',
314                    ]),
315               (2, ['install', 'aboot-base', 'aap-doc', 'ada-reference-manual',
316                    'aspectj-doc', 'fop-doc', 'asis-doc',
317                    'bison-doc', 'crash-whitepaper',
318                    'bash-doc', 'apt-howto-common', 'autotools-dev',
319                    'aptitude-doc-en', 'asr-manpages',
320                    'atomix-data', 'alcovebook-sgml-doc',
321                    'afbackup-common', 'airstrike-common',
322                    ]),
323               ]),
324
325          }
326
327 assert 'all' not in tests
328 assert 'help' not in tests
329
330 CWD = os.getcwd()
331 apt_conf_template = """
332 {
333   // Location of the state dir
334   State "var/lib/apt/"
335   {
336      Lists "lists/";
337      xstatus "xstatus";
338      userstatus "status.user";
339      cdroms "cdroms.list";
340   };
341
342   // Location of the cache dir
343   Cache "var/cache/apt/" {
344      Archives "archives/";
345      srcpkgcache "srcpkgcache.bin";
346      pkgcache "pkgcache.bin";
347   };
348
349   // Config files
350   Etc "etc/apt/" {
351      SourceList "sources.list";
352      Main "apt.conf";
353      Preferences "preferences";
354      Parts "apt.conf.d/";
355   };
356
357   // Locations of binaries
358   Bin {
359      methods "/usr/lib/apt/methods/";
360      gzip "/bin/gzip";
361      gpg  "/usr/bin/gpgv";
362      dpkg "/usr/bin/dpkg --simulate";
363      dpkg-source "/usr/bin/dpkg-source";
364      dpkg-buildpackage "/usr/bin/dpkg-buildpackage";
365      apt-get "/usr/bin/apt-get";
366      apt-cache "/usr/bin/apt-cache";
367   };
368 };
369
370 /* Options you can set to see some debugging text They correspond to names
371    of classes in the source code */
372 Debug
373 {
374   pkgProblemResolver "false";
375   pkgDepCache::AutoInstall "false"; // what packages apt install to satify dependencies
376   pkgAcquire "false";
377   pkgAcquire::Worker "false";
378   pkgAcquire::Auth "false";
379   pkgDPkgPM "false";
380   pkgDPkgProgressReporting "false";
381   pkgOrderList "false";
382   BuildDeps "false";
383
384   pkgInitialize "false";   // This one will dump the configuration space
385   NoLocking "false";
386   Acquire::Ftp "false";    // Show ftp command traffic
387   Acquire::Http "false";   // Show http command traffic
388   Acquire::gpgv "false";   // Show the gpgv traffic
389   aptcdrom "false";        // Show found package files
390   IdentCdrom "false";
391
392 }
393 """
394 apt_p2p_conf_template = """
395 [DEFAULT]
396
397 # Port to listen on for all requests (TCP and UDP)
398 PORT = %(PORT)s
399     
400 # The rate to limit sending data to peers to, in KBytes/sec.
401 # Set this to 0 to not limit the upload bandwidth.
402 UPLOAD_LIMIT = 100
403
404 # The minimum number of peers before the mirror is not used.
405 # If there are fewer peers than this for a file, the mirror will also be
406 # used to speed up the download. Set to 0 to never use the mirror if
407 # there are peers.
408 MIN_DOWNLOAD_PEERS = 3
409
410 # Directory to store the downloaded files in
411 CACHE_DIR = %(CACHE_DIR)s
412     
413 # Other directories containing packages to share with others
414 # WARNING: all files in these directories will be hashed and available
415 #          for everybody to download
416 # OTHER_DIRS = 
417     
418 # Whether it's OK to use an IP addres from a known local/private range
419 LOCAL_OK = yes
420
421 # Whether a remote peer can access the statistics page
422 REMOTE_STATS = yes
423
424 # Unload the packages cache after an interval of inactivity this long.
425 # The packages cache uses a lot of memory, and only takes a few seconds
426 # to reload when a new request arrives.
427 UNLOAD_PACKAGES_CACHE = 5m
428
429 # Refresh the DHT keys after this much time has passed.
430 # This should be a time slightly less than the DHT's KEY_EXPIRE value.
431 KEY_REFRESH = 57m
432
433 # The user name to try and run as (leave blank to run as current user)
434 USERNAME = 
435
436 # Which DHT implementation to use.
437 # It must be possile to do "from <DHT>.DHT import DHT" to get a class that
438 # implements the IDHT interface.
439 DHT = apt_p2p_Khashmir
440
441 # Whether to only run the DHT (for providing only a bootstrap node)
442 DHT-ONLY = %(DHT-ONLY)s
443
444 [apt_p2p_Khashmir]
445 # bootstrap nodes to contact to join the DHT
446 BOOTSTRAP = %(BOOTSTRAP)s
447
448 # whether this node is a bootstrap node
449 BOOTSTRAP_NODE = %(BOOTSTRAP_NODE)s
450
451 # checkpoint every this many seconds
452 CHECKPOINT_INTERVAL = 5m
453
454 # concurrent xmlrpc calls per find node/value request!
455 CONCURRENT_REQS = 4
456
457 # how many hosts to post to
458 STORE_REDUNDANCY = 3
459
460 # How many values to attempt to retrieve from the DHT.
461 # Setting this to 0 will try and get all values (which could take a while if
462 # a lot of nodes have values). Setting it negative will try to get that
463 # number of results from only the closest STORE_REDUNDANCY nodes to the hash.
464 # The default is a large negative number so all values from the closest
465 # STORE_REDUNDANCY nodes will be retrieved.
466 RETRIEVE_VALUES = -10000
467
468 # how many times in a row a node can fail to respond before it's booted from the routing table
469 MAX_FAILURES = 3
470
471 # never ping a node more often than this
472 MIN_PING_INTERVAL = 15m
473
474 # refresh buckets that haven't been touched in this long
475 BUCKET_STALENESS = 1h
476
477 # expire entries older than this
478 KEY_EXPIRE = 1h
479
480 # whether to spew info about the requests/responses in the protocol
481 SPEW = yes
482 """
483
484 def rmrf(top):
485     """Remove all the files and directories below a top-level one.
486     
487     @type top: C{string}
488     @param top: the top-level directory to start at
489     
490     """
491     
492     for root, dirs, files in os.walk(top, topdown=False):
493         for name in files:
494             os.remove(os.path.join(root, name))
495         for name in dirs:
496             os.rmdir(os.path.join(root, name))
497
498 def join(dir):
499     """Join together a list of directories into a path string.
500     
501     @type dir: C{list} of C{string}
502     @param dir: the path to join together
503     @rtype: C{string}
504     @return: the joined together path
505     
506     """
507     
508     joined = ''
509     for i in dir:
510         joined = os.path.join(joined, i)
511     return joined
512
513 def makedirs(dir):
514     """Create all the directories to make a path.
515     
516     @type dir: C{list} of C{string}
517     @param dir: the path to create
518     
519     """
520     if not os.path.exists(join(dir)):
521         os.makedirs(join(dir))
522
523 def touch(path):
524     """Create an empty file.
525     
526     @type path: C{list} of C{string}
527     @param path: the path to create
528     
529     """
530     
531     f = open(join(path), 'w')
532     f.close()
533
534 def start(cmd, args, work_dir = None):
535     """Fork and start a background process running.
536     
537     @type cmd: C{string}
538     @param cmd: the name of the command to run
539     @type args: C{list} of C{string}
540     @param args: the argument to pass to the command
541     @type work_dir: C{string}
542     @param work_dir: the directory to change to to execute the child process in
543         (optional, defaults to the current directory)
544     @rtype: C{int}
545     @return: the PID of the forked process
546     
547     """
548     
549     new_cmd = [cmd] + args
550     pid = os.spawnvp(os.P_NOWAIT, new_cmd[0], new_cmd)
551     return pid
552
553 def stop(pid):
554     """Stop a forked background process that is running.
555     
556     @type pid: C{int}
557     @param pid: the PID of the process to stop
558     @rtype: C{int}
559     @return: the return status code from the child
560     
561     """
562
563     # First try a keyboard interrupt
564     os.kill(pid, signal.SIGINT)
565     for i in xrange(5):
566         sleep(1)
567         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
568         if r_pid:
569             return r_value
570     
571     # Try a keyboard interrupt again, just in case
572     os.kill(pid, signal.SIGINT)
573     for i in xrange(5):
574         sleep(1)
575         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
576         if r_pid:
577             return r_value
578
579     # Try a terminate
580     os.kill(pid, signal.SIGTERM)
581     for i in xrange(5):
582         sleep(1)
583         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
584         if r_pid:
585             return r_value
586
587     # Finally a kill, don't return until killed
588     os.kill(pid, signal.SIGKILL)
589     while not r_pid:
590         sleep(1)
591         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
592
593     return r_value
594
595 def apt_get(num_down, cmd):
596     """Start an apt-get process in the background.
597
598     The default argument specified to the apt-get invocation are
599     'apt-get -d -q -c <conf_file>'. Any additional arguments (including
600     the apt-get action to use) should be specified.
601     
602     @type num_down: C{int}
603     @param num_down: the number of the downloader to use
604     @type cmd: C{list} of C{string}
605     @param cmd: the arguments to pass to the apt-get process
606     @rtype: C{int}
607     @return: the PID of the background process
608     
609     """
610     
611     downloader_dir = down_dir(num_down)
612     rmrf(join([downloader_dir, 'var', 'cache', 'apt', 'archives']))
613     makedirs([downloader_dir, 'var', 'cache', 'apt', 'archives', 'partial'])
614
615     print '*************** apt-get (' + str(num_down) + ') ' + ' '.join(cmd) + ' ****************'
616     apt_conf = join([down_dir(num_down), 'etc', 'apt', 'apt.conf'])
617     dpkg_status = join([down_dir(num_down), 'var', 'lib', 'dpkg', 'status'])
618     args = ['-d', '-c', apt_conf, '-o', 'Dir::state::status='+dpkg_status] + cmd
619     pid = start('apt-get', args)
620     return pid
621
622 def bootstrap_address(num_boot):
623     """Determine the bootstrap address to use for a node.
624     
625     @type num_boot: C{int}
626     @param num_boot: the number of the bootstrap node
627     @rtype: C{string}
628     @return: the bootstrap address to use
629     
630     """
631     
632     return 'localhost:1' + str(num_boot) + '969'
633
634 def down_dir(num_down):
635     """Determine the working directory to use for a downloader.
636     
637     @type num_down: C{int}
638     @param num_down: the number of the downloader
639     @rtype: C{string}
640     @return: the downloader's directory
641     
642     """
643     
644     return os.path.join(CWD,'downloader' + str(num_down))
645
646 def boot_dir(num_boot):
647     """Determine the working directory to use for a bootstrap node.
648     
649     @type num_boot: C{int}
650     @param num_boot: the number of the bootstrap node
651     @rtype: C{string}
652     @return: the bootstrap node's directory
653     
654     """
655     
656     return os.path.join(CWD,'bootstrap' + str(num_boot))
657
658 def start_downloader(bootstrap_addresses, num_down, options = {},
659                      mirror = 'ftp.us.debian.org/debian', 
660                      suites = 'main contrib non-free', clean = True):
661     """Initialize a new downloader process.
662
663     The default arguments specified to the downloader invocation are
664     the configuration directory, apt port, minport, maxport and the
665     maximum upload rate. 
666     Any additional arguments needed should be specified by L{options}.
667     
668     @type num_down: C{int}
669     @param num_down: the number of the downloader to use
670     @type options: C{dictionary}
671     @param options: the dictionary of string formatting values for creating
672         the apt-p2p configuration file (see L{apt_p2p_conf_template} above).
673         (optional, defaults to only using the default arguments)
674     @type mirror: C{string}
675     @param mirror: the Debian mirror to use
676         (optional, defaults to 'ftp.us.debian.org/debian')
677     @type suites: C{string}
678     @param suites: space separated list of suites to download
679         (optional, defaults to 'main contrib non-free')
680     @type clean: C{boolean}
681     @param clean: whether to remove any previous downloader files
682         (optional, defaults to removing them)
683     @rtype: C{int}
684     @return: the PID of the downloader process
685     
686     """
687     
688     assert num_down < 100
689     
690     print '************************** Starting Downloader ' + str(num_down) + ' **************************'
691
692     downloader_dir = down_dir(num_down)
693     
694     if clean:
695         try:
696             rmrf(downloader_dir)
697         except:
698             pass
699     
700     # Create the directory structure needed by apt
701     makedirs([downloader_dir, 'etc', 'apt', 'apt.conf.d'])
702     makedirs([downloader_dir, 'var', 'lib', 'apt', 'lists', 'partial'])
703     makedirs([downloader_dir, 'var', 'lib', 'dpkg'])
704     rmrf(join([downloader_dir, 'var', 'cache', 'apt', 'archives']))
705     makedirs([downloader_dir, 'var', 'cache', 'apt', 'archives', 'partial'])
706     touch([downloader_dir, 'var', 'lib', 'apt', 'lists', 'lock'])
707     touch([downloader_dir, 'var', 'lib', 'dpkg', 'lock'])
708     touch([downloader_dir, 'var', 'lib', 'dpkg', 'status'])
709     touch([downloader_dir, 'var', 'cache', 'apt', 'archives', 'lock'])
710
711     if not exists(join([downloader_dir, 'etc', 'apt', 'sources.list'])):
712         # Create apt's config files
713         f = open(join([downloader_dir, 'etc', 'apt', 'sources.list']), 'w')
714         f.write('deb http://localhost:1%02d77/%s/ unstable %s\n' % (num_down, mirror, suites))
715         f.close()
716
717     if not exists(join([downloader_dir, 'etc', 'apt', 'apt.conf'])):
718         f = open(join([downloader_dir, 'etc', 'apt', 'apt.conf']), 'w')
719         f.write('Dir "' + downloader_dir + '"')
720         f.write(apt_conf_template)
721         f.close()
722
723     defaults = {'PORT': '1%02d77' % num_down,
724                 'CACHE_DIR': downloader_dir,
725                 'DHT-ONLY': 'no',
726                 'BOOTSTRAP': bootstrap_addresses,
727                 'BOOTSTRAP_NODE': 'no'}
728
729     for k in options:
730         defaults[k] = options[k]
731     f = open(join([downloader_dir, 'apt-p2p.conf']), 'w')
732     f.write(apt_p2p_conf_template % defaults)
733     f.close()
734     
735     pid = start('python', [join([sys.path[0], 'apt-p2p.py']),
736                            '--config-file=' + join([downloader_dir, 'apt-p2p.conf']),
737                            '--log-file=' + join([downloader_dir, 'apt-p2p.log']),],
738                 downloader_dir)
739     return pid
740
741 def start_bootstrap(bootstrap_addresses, num_boot, options = [], clean = True):
742     """Initialize a new bootstrap node process.
743
744     The default arguments specified to the apt-p2p invocation are
745     the state file and port to use. Any additional arguments needed 
746     should be specified by L{options}.
747     
748     @type num_boot: C{int}
749     @param num_boot: the number of the bootstrap node to use
750     @type options: C{list} of C{string}
751     @param options: the arguments to pass to the bootstrap node
752         (optional, defaults to only using the default arguments)
753     @type clean: C{boolean}
754     @param clean: whether to remove any previous bootstrap node files
755         (optional, defaults to removing them)
756     @rtype: C{int}
757     @return: the PID of the downloader process
758     
759     """
760     
761     assert num_boot < 10
762
763     print '************************** Starting Bootstrap ' + str(num_boot) + ' **************************'
764
765     bootstrap_dir = boot_dir(num_boot)
766     
767     if clean:
768         try:
769             rmrf(bootstrap_dir)
770         except:
771             pass
772
773     makedirs([bootstrap_dir])
774
775     defaults = {'PORT': '1%d969' % num_boot,
776                 'CACHE_DIR': bootstrap_dir,
777                 'DHT-ONLY': 'yes',
778                 'BOOTSTRAP': bootstrap_addresses,
779                 'BOOTSTRAP_NODE': 'yes'}
780
781     for k in options:
782         defaults[k] = options[k]
783     f = open(join([bootstrap_dir, 'apt-p2p.conf']), 'w')
784     f.write(apt_p2p_conf_template % defaults)
785     f.close()
786     
787     pid = start('python', [join([sys.path[0], 'apt-p2p.py']),
788                            '--config-file=' + join([bootstrap_dir, 'apt-p2p.conf']),
789                            '--log-file=' + join([bootstrap_dir, 'apt-p2p.log']),],
790                 bootstrap_dir)
791
792     return pid
793
794 def run_test(bootstraps, downloaders, apt_get_queue):
795     """Run a single test.
796     
797     @type bootstraps: C{dictionary} of {C{int}: C{list} of C{string}}
798     @param bootstraps: the bootstrap nodes to start, keys are the bootstrap numbers and
799         values are the list of options to invoke the bootstrap node with
800     @type downloaders: C{dictionary} of {C{int}: (C{int}, C{list} of C{string})}
801     @param downloaders: the downloaders to start, keys are the downloader numbers and
802         values are the list of options to invoke the downloader with
803     @type apt_get_queue: C{list} of (C{int}, C{list} of C{string})
804     @param apt_get_queue: the apt-get downloader to use and commands to execute
805     @rtype: C{list} of (C{float}, C{int})
806     @return: the execution time and returned status code for each element of apt_get_queue
807     
808     """
809     
810     running_bootstraps = {}
811     running_downloaders = {}
812     running_apt_get = {}
813     apt_get_results = []
814
815     try:
816         boot_keys = bootstraps.keys()
817         boot_keys.sort()
818         bootstrap_addresses = bootstrap_address(boot_keys[0])
819         for i in xrange(1, len(boot_keys)):
820             bootstrap_addresses += '\n      ' + bootstrap_address(boot_keys[i])
821             
822         for k, v in bootstraps.items():
823             running_bootstraps[k] = start_bootstrap(bootstrap_addresses, k, **v)
824         
825         sleep(5)
826         
827         for k, v in downloaders.items():
828             running_downloaders[k] = start_downloader(bootstrap_addresses, k, **v)
829     
830         sleep(5)
831         
832         for (num_down, cmd) in apt_get_queue:
833             running_apt_get[num_down] = apt_get(num_down, cmd)
834             start_time = time()
835             (pid, r_value) = os.waitpid(running_apt_get[num_down], 0)
836             elapsed = time() - start_time
837             del running_apt_get[num_down]
838             r_value = r_value / 256
839             apt_get_results.append((elapsed, r_value))
840
841             if r_value == 0:
842                 print '********** apt-get completed successfully in ' +  str(elapsed) + ' sec. *****************'
843             else:
844                 print '********** apt-get finished with status ' + str(r_value) + ' in ' +  str(elapsed) + ' sec. ************'
845         
846             sleep(5)
847             
848     except:
849         print '************************** Exception occurred **************************'
850         print_exc()
851         print '************************** will attempt to shut down *******************'
852         
853     print '*********************** shutting down the apt-gets *******************'
854     for k, v in running_apt_get.items():
855         try:
856             print 'apt-get', k, stop(v)
857         except:
858             print '************************** Exception occurred **************************'
859             print_exc()
860
861     sleep(5)
862
863     print '*********************** shutting down the downloaders *******************'
864     for k, v in running_downloaders.items():
865         try:
866             print 'downloader', k, stop(v)
867         except:
868             print '************************** Exception occurred **************************'
869             print_exc()
870
871     sleep(5)
872
873     print '************************** shutting down the bootstraps *******************'
874     for k, v in running_bootstraps.items():
875         try:
876             print 'bootstrap', k, stop(v)
877         except:
878             print '************************** Exception occurred **************************'
879             print_exc()
880
881     print '************************** Test Results *******************'
882     i = -1
883     for (num_down, cmd) in apt_get_queue:
884         i += 1
885         s = str(num_down) + ': "apt-get ' + ' '.join(cmd) + '" '
886         if len(apt_get_results) > i:
887             (elapsed, r_value) = apt_get_results[i]
888             s += 'took ' + str(elapsed) + ' secs (' + str(r_value) + ')'
889         else:
890             s += 'did not complete'
891         print s
892     
893     return apt_get_results
894
895 def get_usage():
896     """Get the usage information to display to the user.
897     
898     @rtype: C{string}
899     @return: the usage information to display
900     
901     """
902     
903     s = 'Usage: ' + sys.argv[0] + ' (all|<test>|help)\n\n'
904     s += '  all    - run all the tests\n'
905     s += '  help   - display this usage information\n'
906     s += '  <test> - run the <test> test (see list below for valid tests)\n\n'
907     
908     t = tests.items()
909     t.sort()
910     for k, v in t:
911         s += 'test "' + str(k) + '" - ' + v[0] + '\n'
912     
913     return s
914
915 if __name__ == '__main__':
916     if len(sys.argv) != 2:
917         print get_usage()
918     elif sys.argv[1] == 'all':
919         for k, v in tests.items():
920             run_test(v[1], v[2], v[3])
921     elif sys.argv[1] in tests:
922         v = tests[sys.argv[1]]
923         run_test(v[1], v[2], v[3])
924     elif sys.argv[1] == 'help':
925         print get_usage()
926     else:
927         print 'Unknown test to run:', sys.argv[1], '\n'
928         print get_usage()
929