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