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