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