Set the Last-Modified header when downloading from peers.
[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 # Timeout KRPC requests to nodes after this time.
481 KRPC_TIMEOUT = 14s
482
483 # KRPC requests are resent using exponential backoff starting with this delay.
484 # The request will first be resent after the delay set here.
485 # The request will be resent again after twice the delay set here. etc.
486 # e.g. if TIMEOUT is 14 sec., and INITIAL_DELAY is 2 sec., then requests will
487 # be resent at times 0, 2 (2 sec. later), and 6 (4 sec. later), and then will
488 # timeout at 14.
489 KRPC_INITIAL_DELAY = 2s
490
491 # whether to spew info about the requests/responses in the protocol
492 SPEW = yes
493 """
494
495 def rmrf(top):
496     """Remove all the files and directories below a top-level one.
497     
498     @type top: C{string}
499     @param top: the top-level directory to start at
500     
501     """
502     
503     for root, dirs, files in os.walk(top, topdown=False):
504         for name in files:
505             os.remove(os.path.join(root, name))
506         for name in dirs:
507             os.rmdir(os.path.join(root, name))
508
509 def join(dir):
510     """Join together a list of directories into a path string.
511     
512     @type dir: C{list} of C{string}
513     @param dir: the path to join together
514     @rtype: C{string}
515     @return: the joined together path
516     
517     """
518     
519     joined = ''
520     for i in dir:
521         joined = os.path.join(joined, i)
522     return joined
523
524 def makedirs(dir):
525     """Create all the directories to make a path.
526     
527     @type dir: C{list} of C{string}
528     @param dir: the path to create
529     
530     """
531     if not os.path.exists(join(dir)):
532         os.makedirs(join(dir))
533
534 def touch(path):
535     """Create an empty file.
536     
537     @type path: C{list} of C{string}
538     @param path: the path to create
539     
540     """
541     
542     f = open(join(path), 'w')
543     f.close()
544
545 def start(cmd, args, work_dir = None):
546     """Fork and start a background process running.
547     
548     @type cmd: C{string}
549     @param cmd: the name of the command to run
550     @type args: C{list} of C{string}
551     @param args: the argument to pass to the command
552     @type work_dir: C{string}
553     @param work_dir: the directory to change to to execute the child process in
554         (optional, defaults to the current directory)
555     @rtype: C{int}
556     @return: the PID of the forked process
557     
558     """
559     
560     new_cmd = [cmd] + args
561     pid = os.spawnvp(os.P_NOWAIT, new_cmd[0], new_cmd)
562     return pid
563
564 def stop(pid):
565     """Stop a forked background process that is running.
566     
567     @type pid: C{int}
568     @param pid: the PID of the process to stop
569     @rtype: C{int}
570     @return: the return status code from the child
571     
572     """
573
574     # First try a keyboard interrupt
575     os.kill(pid, signal.SIGINT)
576     for i in xrange(5):
577         sleep(1)
578         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
579         if r_pid:
580             return r_value
581     
582     # Try a keyboard interrupt again, just in case
583     os.kill(pid, signal.SIGINT)
584     for i in xrange(5):
585         sleep(1)
586         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
587         if r_pid:
588             return r_value
589
590     # Try a terminate
591     os.kill(pid, signal.SIGTERM)
592     for i in xrange(5):
593         sleep(1)
594         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
595         if r_pid:
596             return r_value
597
598     # Finally a kill, don't return until killed
599     os.kill(pid, signal.SIGKILL)
600     while not r_pid:
601         sleep(1)
602         (r_pid, r_value) = os.waitpid(pid, os.WNOHANG)
603
604     return r_value
605
606 def apt_get(num_down, cmd):
607     """Start an apt-get process in the background.
608
609     The default argument specified to the apt-get invocation are
610     'apt-get -d -q -c <conf_file>'. Any additional arguments (including
611     the apt-get action to use) should be specified.
612     
613     @type num_down: C{int}
614     @param num_down: the number of the downloader to use
615     @type cmd: C{list} of C{string}
616     @param cmd: the arguments to pass to the apt-get process
617     @rtype: C{int}
618     @return: the PID of the background process
619     
620     """
621     
622     downloader_dir = down_dir(num_down)
623     rmrf(join([downloader_dir, 'var', 'cache', 'apt', 'archives']))
624     makedirs([downloader_dir, 'var', 'cache', 'apt', 'archives', 'partial'])
625
626     print '*************** apt-get (' + str(num_down) + ') ' + ' '.join(cmd) + ' ****************'
627     apt_conf = join([down_dir(num_down), 'etc', 'apt', 'apt.conf'])
628     dpkg_status = join([down_dir(num_down), 'var', 'lib', 'dpkg', 'status'])
629     args = ['-d', '-c', apt_conf, '-o', 'Dir::state::status='+dpkg_status] + cmd
630     pid = start('apt-get', args)
631     return pid
632
633 def bootstrap_address(num_boot):
634     """Determine the bootstrap address to use for a node.
635     
636     @type num_boot: C{int}
637     @param num_boot: the number of the bootstrap node
638     @rtype: C{string}
639     @return: the bootstrap address to use
640     
641     """
642     
643     return 'localhost:1' + str(num_boot) + '969'
644
645 def down_dir(num_down):
646     """Determine the working directory to use for a downloader.
647     
648     @type num_down: C{int}
649     @param num_down: the number of the downloader
650     @rtype: C{string}
651     @return: the downloader's directory
652     
653     """
654     
655     return os.path.join(CWD,'downloader' + str(num_down))
656
657 def boot_dir(num_boot):
658     """Determine the working directory to use for a bootstrap node.
659     
660     @type num_boot: C{int}
661     @param num_boot: the number of the bootstrap node
662     @rtype: C{string}
663     @return: the bootstrap node's directory
664     
665     """
666     
667     return os.path.join(CWD,'bootstrap' + str(num_boot))
668
669 def start_downloader(bootstrap_addresses, num_down, options = {},
670                      mirror = 'ftp.us.debian.org/debian', 
671                      suites = 'main contrib non-free', clean = True):
672     """Initialize a new downloader process.
673
674     The default arguments specified to the downloader invocation are
675     the configuration directory, apt port, minport, maxport and the
676     maximum upload rate. 
677     Any additional arguments needed should be specified by L{options}.
678     
679     @type num_down: C{int}
680     @param num_down: the number of the downloader to use
681     @type options: C{dictionary}
682     @param options: the dictionary of string formatting values for creating
683         the apt-p2p configuration file (see L{apt_p2p_conf_template} above).
684         (optional, defaults to only using the default arguments)
685     @type mirror: C{string}
686     @param mirror: the Debian mirror to use
687         (optional, defaults to 'ftp.us.debian.org/debian')
688     @type suites: C{string}
689     @param suites: space separated list of suites to download
690         (optional, defaults to 'main contrib non-free')
691     @type clean: C{boolean}
692     @param clean: whether to remove any previous downloader files
693         (optional, defaults to removing them)
694     @rtype: C{int}
695     @return: the PID of the downloader process
696     
697     """
698     
699     assert num_down < 100
700     
701     print '************************** Starting Downloader ' + str(num_down) + ' **************************'
702
703     downloader_dir = down_dir(num_down)
704     
705     if clean:
706         try:
707             rmrf(downloader_dir)
708         except:
709             pass
710     
711     # Create the directory structure needed by apt
712     makedirs([downloader_dir, 'etc', 'apt', 'apt.conf.d'])
713     makedirs([downloader_dir, 'var', 'lib', 'apt', 'lists', 'partial'])
714     makedirs([downloader_dir, 'var', 'lib', 'dpkg'])
715     rmrf(join([downloader_dir, 'var', 'cache', 'apt', 'archives']))
716     makedirs([downloader_dir, 'var', 'cache', 'apt', 'archives', 'partial'])
717     touch([downloader_dir, 'var', 'lib', 'apt', 'lists', 'lock'])
718     touch([downloader_dir, 'var', 'lib', 'dpkg', 'lock'])
719     touch([downloader_dir, 'var', 'lib', 'dpkg', 'status'])
720     touch([downloader_dir, 'var', 'cache', 'apt', 'archives', 'lock'])
721
722     if not exists(join([downloader_dir, 'etc', 'apt', 'sources.list'])):
723         # Create apt's config files
724         f = open(join([downloader_dir, 'etc', 'apt', 'sources.list']), 'w')
725         f.write('deb http://localhost:1%02d77/%s/ unstable %s\n' % (num_down, mirror, suites))
726         f.close()
727
728     if not exists(join([downloader_dir, 'etc', 'apt', 'apt.conf'])):
729         f = open(join([downloader_dir, 'etc', 'apt', 'apt.conf']), 'w')
730         f.write('Dir "' + downloader_dir + '"')
731         f.write(apt_conf_template)
732         f.close()
733
734     defaults = {'PORT': '1%02d77' % num_down,
735                 'CACHE_DIR': downloader_dir,
736                 'DHT-ONLY': 'no',
737                 'BOOTSTRAP': bootstrap_addresses,
738                 'BOOTSTRAP_NODE': 'no'}
739
740     for k in options:
741         defaults[k] = options[k]
742     f = open(join([downloader_dir, 'apt-p2p.conf']), 'w')
743     f.write(apt_p2p_conf_template % defaults)
744     f.close()
745     
746     pid = start('python', [join([sys.path[0], 'apt-p2p.py']),
747                            '--config-file=' + join([downloader_dir, 'apt-p2p.conf']),
748                            '--log-file=' + join([downloader_dir, 'apt-p2p.log']),],
749                 downloader_dir)
750     return pid
751
752 def start_bootstrap(bootstrap_addresses, num_boot, options = [], clean = True):
753     """Initialize a new bootstrap node process.
754
755     The default arguments specified to the apt-p2p invocation are
756     the state file and port to use. Any additional arguments needed 
757     should be specified by L{options}.
758     
759     @type num_boot: C{int}
760     @param num_boot: the number of the bootstrap node to use
761     @type options: C{list} of C{string}
762     @param options: the arguments to pass to the bootstrap node
763         (optional, defaults to only using the default arguments)
764     @type clean: C{boolean}
765     @param clean: whether to remove any previous bootstrap node files
766         (optional, defaults to removing them)
767     @rtype: C{int}
768     @return: the PID of the downloader process
769     
770     """
771     
772     assert num_boot < 10
773
774     print '************************** Starting Bootstrap ' + str(num_boot) + ' **************************'
775
776     bootstrap_dir = boot_dir(num_boot)
777     
778     if clean:
779         try:
780             rmrf(bootstrap_dir)
781         except:
782             pass
783
784     makedirs([bootstrap_dir])
785
786     defaults = {'PORT': '1%d969' % num_boot,
787                 'CACHE_DIR': bootstrap_dir,
788                 'DHT-ONLY': 'yes',
789                 'BOOTSTRAP': bootstrap_addresses,
790                 'BOOTSTRAP_NODE': 'yes'}
791
792     for k in options:
793         defaults[k] = options[k]
794     f = open(join([bootstrap_dir, 'apt-p2p.conf']), 'w')
795     f.write(apt_p2p_conf_template % defaults)
796     f.close()
797     
798     pid = start('python', [join([sys.path[0], 'apt-p2p.py']),
799                            '--config-file=' + join([bootstrap_dir, 'apt-p2p.conf']),
800                            '--log-file=' + join([bootstrap_dir, 'apt-p2p.log']),],
801                 bootstrap_dir)
802
803     return pid
804
805 def run_test(bootstraps, downloaders, apt_get_queue):
806     """Run a single test.
807     
808     @type bootstraps: C{dictionary} of {C{int}: C{list} of C{string}}
809     @param bootstraps: the bootstrap nodes to start, keys are the bootstrap numbers and
810         values are the list of options to invoke the bootstrap node with
811     @type downloaders: C{dictionary} of {C{int}: (C{int}, C{list} of C{string})}
812     @param downloaders: the downloaders to start, keys are the downloader numbers and
813         values are the list of options to invoke the downloader with
814     @type apt_get_queue: C{list} of (C{int}, C{list} of C{string})
815     @param apt_get_queue: the apt-get downloader to use and commands to execute
816     @rtype: C{list} of (C{float}, C{int})
817     @return: the execution time and returned status code for each element of apt_get_queue
818     
819     """
820     
821     running_bootstraps = {}
822     running_downloaders = {}
823     running_apt_get = {}
824     apt_get_results = []
825
826     try:
827         boot_keys = bootstraps.keys()
828         boot_keys.sort()
829         bootstrap_addresses = bootstrap_address(boot_keys[0])
830         for i in xrange(1, len(boot_keys)):
831             bootstrap_addresses += '\n      ' + bootstrap_address(boot_keys[i])
832             
833         for k, v in bootstraps.items():
834             running_bootstraps[k] = start_bootstrap(bootstrap_addresses, k, **v)
835         
836         sleep(5)
837         
838         for k, v in downloaders.items():
839             running_downloaders[k] = start_downloader(bootstrap_addresses, k, **v)
840     
841         sleep(5)
842         
843         for (num_down, cmd) in apt_get_queue:
844             running_apt_get[num_down] = apt_get(num_down, cmd)
845             start_time = time()
846             (pid, r_value) = os.waitpid(running_apt_get[num_down], 0)
847             elapsed = time() - start_time
848             del running_apt_get[num_down]
849             r_value = r_value / 256
850             apt_get_results.append((elapsed, r_value))
851
852             if r_value == 0:
853                 print '********** apt-get completed successfully in ' +  str(elapsed) + ' sec. *****************'
854             else:
855                 print '********** apt-get finished with status ' + str(r_value) + ' in ' +  str(elapsed) + ' sec. ************'
856         
857             sleep(5)
858             
859     except:
860         print '************************** Exception occurred **************************'
861         print_exc()
862         print '************************** will attempt to shut down *******************'
863         
864     print '*********************** shutting down the apt-gets *******************'
865     for k, v in running_apt_get.items():
866         try:
867             print 'apt-get', k, stop(v)
868         except:
869             print '************************** Exception occurred **************************'
870             print_exc()
871
872     sleep(5)
873
874     print '*********************** shutting down the downloaders *******************'
875     for k, v in running_downloaders.items():
876         try:
877             print 'downloader', k, stop(v)
878         except:
879             print '************************** Exception occurred **************************'
880             print_exc()
881
882     sleep(5)
883
884     print '************************** shutting down the bootstraps *******************'
885     for k, v in running_bootstraps.items():
886         try:
887             print 'bootstrap', k, stop(v)
888         except:
889             print '************************** Exception occurred **************************'
890             print_exc()
891
892     print '************************** Test Results *******************'
893     i = -1
894     for (num_down, cmd) in apt_get_queue:
895         i += 1
896         s = str(num_down) + ': "apt-get ' + ' '.join(cmd) + '" '
897         if len(apt_get_results) > i:
898             (elapsed, r_value) = apt_get_results[i]
899             s += 'took ' + str(elapsed) + ' secs (' + str(r_value) + ')'
900         else:
901             s += 'did not complete'
902         print s
903     
904     return apt_get_results
905
906 def get_usage():
907     """Get the usage information to display to the user.
908     
909     @rtype: C{string}
910     @return: the usage information to display
911     
912     """
913     
914     s = 'Usage: ' + sys.argv[0] + ' (all|<test>|help)\n\n'
915     s += '  all    - run all the tests\n'
916     s += '  help   - display this usage information\n'
917     s += '  <test> - run the <test> test (see list below for valid tests)\n\n'
918     
919     t = tests.items()
920     t.sort()
921     for k, v in t:
922         s += 'test "' + str(k) + '" - ' + v[0] + '\n'
923     
924     return s
925
926 if __name__ == '__main__':
927     if len(sys.argv) != 2:
928         print get_usage()
929     elif sys.argv[1] == 'all':
930         for k, v in tests.items():
931             run_test(v[1], v[2], v[3])
932     elif sys.argv[1] in tests:
933         v = tests[sys.argv[1]]
934         run_test(v[1], v[2], v[3])
935     elif sys.argv[1] == 'help':
936         print get_usage()
937     else:
938         print 'Unknown test to run:', sys.argv[1], '\n'
939         print get_usage()
940