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