~ubuntu-branches/ubuntu/raring/boost-build/raring

« back to all changes in this revision

Viewing changes to test/BoostBuild.py

  • Committer: Bazaar Package Importer
  • Author(s): Steve M. Robbins
  • Date: 2008-08-06 00:38:31 UTC
  • mfrom: (4.1.1 intrepid)
  • Revision ID: james.westby@ubuntu.com-20080806003831-zr65893244swds0b
Tags: 2.0-m12-2
* debian/rules: Do not install /etc/user-config.jam.
* debian/site-config.jam: New.  Install into /etc instead of empty
  example.  Closes: #493323.

* debian/control: Update homepage.  Update description.  Closes:
  #493510.  Update Standards-Version to 3.8.0; no changes.

* debian/compat: New.  Set compat level to 7.
* debian/rules: Remove DH_COMPAT setting.
* debian/control: Change debhelper build-dep to version >= 7.

* debian/control: Remove docbook-to-man, bison from build-deps.

* debian/rules: Clean up upstream source by removing debian/conffiles.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2002-2005 Vladimir Prus.
 
2
# Copyright 2002-2003 Dave Abrahams.
 
3
# Copyright 2006 Rene Rivera.
 
4
# Distributed under the Boost Software License, Version 1.0.
 
5
#    (See accompanying file LICENSE_1_0.txt or copy at
 
6
#         http://www.boost.org/LICENSE_1_0.txt)
1
7
 
2
8
import TestCmd
3
9
from tree import build_tree, trees_difference
4
10
import copy
5
11
import fnmatch
 
12
import glob
6
13
import os
 
14
import re
7
15
import shutil
8
16
import string
9
17
import types
10
18
import time
11
19
import tempfile
12
20
import sys
13
 
import re
 
21
import traceback
 
22
import math
 
23
from StringIO import StringIO
 
24
 
 
25
annotation_func = None
 
26
 
 
27
annotations = []
 
28
 
 
29
def print_annotation(name, value):
 
30
    """Writes some named bit of information about test
 
31
    run.
 
32
    """
 
33
    print name + " {{{"
 
34
    print value
 
35
    print "}}}"
 
36
 
 
37
 
 
38
def flush_annotations():
 
39
    global annotations
 
40
    for ann in annotations:
 
41
        print_annotation(ann[0], ann[1])
 
42
    annotations = []
 
43
 
 
44
defer_annotations = 0
 
45
 
 
46
def set_defer_annotations(n):
 
47
    global defer_annotations
 
48
    defer_annotations = n
 
49
 
 
50
def annotation(name, value):
 
51
    """Records an annotation about test run."""
 
52
    annotations.append((name, value))
 
53
    if not defer_annotations:
 
54
        flush_annotations()
14
55
 
15
56
def get_toolset():
16
57
    toolset = None;
39
80
    if os.__dict__.has_key('uname') and os.uname()[0] == 'Darwin':
40
81
        suffixes['.dll'] = '.dylib'
41
82
 
 
83
def re_remove(sequence,regex):
 
84
    me = re.compile(regex)
 
85
    result = filter( lambda x: me.match(x), sequence )
 
86
    if 0 == len(result):
 
87
        raise ValueError()
 
88
    for r in result:
 
89
        sequence.remove(r)
 
90
 
 
91
def glob_remove(sequence,pattern):
 
92
    result = fnmatch.filter(sequence,pattern)
 
93
    if 0 == len(result):
 
94
        raise ValueError()
 
95
    for r in result:
 
96
        sequence.remove(r)
 
97
 
42
98
lib_prefix = 1
 
99
dll_prefix = 1
43
100
if windows:
44
 
    lib_prefix = 0
45
 
        
 
101
    #~ lib_prefix = 0
 
102
    dll_prefix = 0
46
103
    
47
104
    
48
105
#
60
117
        if os.WIFEXITED(self.status):
61
118
            return os.WEXITSTATUS(self.status)
62
119
        else:
63
 
            return None
 
120
            return -1
64
121
elif os.name == 'nt':
65
122
    def _failed(self, status = 0):
66
123
        return not self.status is None and self.status != status
92
149
 
93
150
        self.toolset = get_toolset()
94
151
        self.pass_toolset = pass_toolset
95
 
        
 
152
 
96
153
        prepare_suffix_map(pass_toolset and self.toolset or 'gcc')
97
154
 
98
 
        jam_build_dir = ""
99
 
        if os.name == 'nt':
100
 
            jam_build_dir = "bin.ntx86"
101
 
        elif os.name == 'posix' and os.__dict__.has_key('uname'):
102
 
            if os.uname()[0].lower().startswith('cygwin'):
103
 
                jam_build_dir = "bin.cygwinx86"
104
 
                if 'TMP' in os.environ and os.environ['TMP'].find('~') != -1:
105
 
                    print 'Setting $TMP to /tmp to get around problem with short path names'
106
 
                    os.environ['TMP'] = '/tmp'
107
 
            elif os.uname()[0] == 'Linux':
108
 
                cpu = os.uname()[4]
109
 
                if re.match("i.86", cpu):
110
 
                    jam_build_dir = "bin.linuxx86";
 
155
        if not '--default-bjam' in sys.argv:
 
156
            jam_build_dir = ""
 
157
            if os.name == 'nt':
 
158
                jam_build_dir = "bin.ntx86"
 
159
            elif os.name == 'posix' and os.__dict__.has_key('uname'):
 
160
                if os.uname()[0].lower().startswith('cygwin'):
 
161
                    jam_build_dir = "bin.cygwinx86"
 
162
                    if 'TMP' in os.environ and os.environ['TMP'].find('~') != -1:
 
163
                        print 'Setting $TMP to /tmp to get around problem with short path names'
 
164
                        os.environ['TMP'] = '/tmp'
 
165
                elif os.uname()[0] == 'Linux':
 
166
                    cpu = os.uname()[4]
 
167
                    if re.match("i.86", cpu):
 
168
                        jam_build_dir = "bin.linuxx86";
 
169
                    else:
 
170
                        jam_build_dir = "bin.linux" + os.uname()[4]
 
171
                elif os.uname()[0] == 'SunOS':
 
172
                    jam_build_dir = "bin.solaris"
 
173
                elif os.uname()[0] == 'Darwin':
 
174
                    jam_build_dir = "bin.macosxppc"
 
175
                elif os.uname()[0] == "AIX":
 
176
                    jam_build_dir = "bin.aix"
 
177
                elif os.uname()[0] == "IRIX64":
 
178
                    jam_build_dir = "bin.irix"
 
179
                elif os.uname()[0] == "FreeBSD":
 
180
                    jam_build_dir = "bin.freebsd"
 
181
                elif os.uname()[0] == "OSF1":
 
182
                    jam_build_dir = "bin.osf"
111
183
                else:
112
 
                    jam_build_dir = "bin.linux" + os.uname()[4]
113
 
            elif os.uname()[0] == 'SunOS':
114
 
                jam_build_dir = "bin.solaris"
115
 
            elif os.uname()[0] == 'Darwin':
116
 
                jam_build_dir = "bin.macosxppc"
117
 
            elif os.uname()[0] == "AIX":
118
 
                jam_build_dir = "bin.aix"
119
 
            elif os.uname()[0] == "IRIX64":
120
 
                jam_build_dir = "bin.irix"
121
 
            elif os.uname()[0] == "FreeBSD":
122
 
                jam_build_dir = "bin.freebsd"
123
 
            elif os.uname()[0] == "OSF1":
124
 
                jam_build_dir = "bin.osf"
125
 
            else:
126
 
                raise "Don't know directory where jam is build for this system: " + os.name + "/" + os.uname()[0]
127
 
        else:
128
 
            raise "Don't know directory where jam is build for this system: " + os.name
129
 
 
130
 
        if boost_build_path is None:
131
 
            boost_build_path = self.original_workdir
132
 
            
 
184
                    raise "Don't know directory where jam is build for this system: " + os.name + "/" + os.uname()[0]
 
185
            else:
 
186
                raise "Don't know directory where jam is build for this system: " + os.name
 
187
 
 
188
            # Find there jam_src is located.
 
189
            # try for the debug version if it's lying around
 
190
 
 
191
            dirs = [os.path.join('../../../jam/src', jam_build_dir + '.debug'),
 
192
                    os.path.join('../../../jam/src', jam_build_dir),
 
193
                    os.path.join('../../jam_src', jam_build_dir + '.debug'),
 
194
                    os.path.join('../../jam_src', jam_build_dir),
 
195
                    os.path.join('../jam_src', jam_build_dir + '.debug'),
 
196
                    os.path.join('../jam_src', jam_build_dir),
 
197
                    ]
 
198
 
 
199
            for d in dirs:
 
200
                if os.path.exists(d):
 
201
                    jam_build_dir = d
 
202
                    break
 
203
            else:
 
204
                print "Cannot find built Boost.Jam"
 
205
                os.exit(1)
133
206
 
134
207
        verbosity = ['-d0', '--quiet']
135
208
        if '--verbose' in sys.argv:
136
209
            keywords['verbose'] = 1
137
210
            verbosity = ['-d+2']
138
211
 
 
212
        if boost_build_path is None:
 
213
            boost_build_path = self.original_workdir
 
214
 
139
215
        program_list = []
140
 
 
141
 
        # Find there jam_src is located.
142
 
        # try for the debug version if it's lying around
143
 
 
144
 
        dirs = [os.path.join('../../../jam/src', jam_build_dir + '.debug'),
145
 
                os.path.join('../../../jam/src', jam_build_dir),
146
 
                os.path.join('../../jam_src', jam_build_dir + '.debug'),
147
 
                os.path.join('../../jam_src', jam_build_dir),
148
 
                os.path.join('../jam_src', jam_build_dir + '.debug'),
149
 
                os.path.join('../jam_src', jam_build_dir),
150
 
                ]
151
 
 
152
 
        for d in dirs:
153
 
            if os.path.exists(d):
154
 
                jam_build_dir = d
155
 
                break
 
216
        
 
217
        if '--default-bjam' in sys.argv:
 
218
            program_list.append(executable)
 
219
            inpath_bjam = True
156
220
        else:
157
 
            print "Cannot find built Boost.Jam"
158
 
            os.exit(1)                                    
159
 
        
160
 
            
161
 
        program_list.append(os.path.join(jam_build_dir, executable))
 
221
            program_list.append(os.path.join(jam_build_dir, executable))
 
222
            inpath_bjam = None
162
223
        program_list.append('-sBOOST_BUILD_PATH=' + boost_build_path)
163
224
        if verbosity:
164
225
            program_list += verbosity
 
226
        program_list += ["--ignore-toolset-requirements"]            
165
227
        if arguments:
166
228
            program_list += arguments.split(" ")
167
229
 
170
232
            , program=program_list
171
233
            , match=match
172
234
            , workdir = workdir
 
235
            , inpath = inpath_bjam
173
236
            , **keywords)
174
237
 
175
238
        os.chdir(self.workdir)
232
295
 
233
296
    def copy(self, src, dst):
234
297
        self.wait_for_time_change()
 
298
        try:
 
299
            self.write(dst, self.read(src))
 
300
        except:
 
301
            self.fail_test(1)
 
302
 
 
303
    def copy_preserving_timestamp(self, src, dst):
 
304
        src_name = self.native_file_name(src)
 
305
        dst_name = self.native_file_name(dst)
 
306
        stats = os.stat(src_name)        
235
307
        self.write(dst, self.read(src))
236
 
 
 
308
        os.utime(dst_name, (stats.st_atime, stats.st_mtime))
 
309
        
237
310
    def touch(self, names):
238
311
        self.wait_for_time_change()
239
312
        for name in self.adjust_names(names):
248
321
        os.chdir(self.original_workdir)
249
322
        for name in names:
250
323
            n = self.native_file_name(name)
251
 
            if os.path.isdir(n):
252
 
                shutil.rmtree(n, ignore_errors=0)
253
 
            else:
254
 
                os.unlink(n)
 
324
            n = glob.glob(n)
 
325
            if n: n = n[0]
 
326
            if not n:
 
327
                n = self.glob_file(string.replace(name, "$toolset", self.toolset+"*"))
 
328
            if n:
 
329
                if os.path.isdir(n):
 
330
                    shutil.rmtree(n, ignore_errors=0)
 
331
                else:
 
332
                    os.unlink(n)
255
333
 
256
334
        # Create working dir root again, in case
257
335
        # we've removed it
266
344
        self.write(name, content)
267
345
                                                        
268
346
    def dump_stdio(self):
269
 
        print "STDOUT ============"
270
 
        print self.stdout()    
271
 
        print "STDERR ============"
272
 
        print self.stderr()
273
 
        print "END ==============="
 
347
        annotation("STDOUT", self.stdout())
 
348
        annotation("STDERR", self.stderr())
274
349
                    
275
350
    #
276
351
    #   FIXME: Large portion copied from TestSCons.py, should be moved?
311
386
            if status != 0:
312
387
                expect = " (expected %d)" % status
313
388
 
314
 
            print '"%s" returned %d%s' % (
315
 
                kw['program'], _status(self), expect)
 
389
            annotation("failed command", '"%s" returned %d%s' % (
 
390
                kw['program'], _status(self), expect))
316
391
 
 
392
            annotation("reason", "error returned by bjam")
317
393
            self.fail_test(1)
318
394
 
319
395
        if not stdout is None and not match(self.stdout(), stdout):
320
 
            print "Expected STDOUT =========="
321
 
            print stdout
322
 
            print "Actual STDOUT ============"
323
 
            print self.stdout()
 
396
            annotation("reason", "Unexpected stdout")
 
397
            annotation("Expected STDOUT", stdout)
 
398
            annotation("Actual STDOUT", self.stdout())
324
399
            stderr = self.stderr()
325
400
            if stderr:
326
 
                print "STDERR ==================="
327
 
                print stderr
 
401
                annotation("STDERR", stderr)
328
402
            self.maybe_do_diff(self.stdout(), stdout)
329
403
            self.fail_test(1, dump_stdio = 0)
330
404
 
334
408
        actual_stderr = re.sub(intel_workaround, "", self.stderr())
335
409
 
336
410
        if not stderr is None and not match(actual_stderr, stderr):
337
 
            print "STDOUT ==================="
338
 
            print self.stdout()
339
 
            print "Expected STDERR =========="
340
 
            print stderr
341
 
            print "Actual STDERR ============"
342
 
            print actual_stderr
 
411
            annotation("reason", "Unexpected stderr")
 
412
            annotation("Expected STDERR", stderr)
 
413
            annotation("Actual STDERR", self.stderr())
 
414
            annotation("STDOUT", self.stdout())
343
415
            self.maybe_do_diff(actual_stderr, stderr)
344
416
            self.fail_test(1, dump_stdio = 0)
345
417
 
349
421
        self.unexpected_difference = copy.deepcopy(self.difference)
350
422
 
351
423
        self.last_build_time = time.time()
 
424
    
 
425
    def glob_file(self, name):
 
426
        result = None
 
427
        if hasattr(self,'difference'):
 
428
            for f in self.difference.added_files+self.difference.modified_files+self.difference.touched_files:
 
429
                if fnmatch.fnmatch(f,name):
 
430
                    result = self.native_file_name(f)
 
431
                    break
 
432
        if not result:
 
433
            result = glob.glob(self.native_file_name(name))
 
434
            if result:
 
435
                result = result[0]
 
436
        return result
352
437
 
353
438
    def read(self, name):
354
 
        return open(self.native_file_name(name), "rb").read()
 
439
        try:
 
440
            if self.toolset:
 
441
                name = string.replace(name, "$toolset", self.toolset+"*")
 
442
            name = self.glob_file(name)
 
443
            return open(name, "rU").read()
 
444
        except:
 
445
            annotation("reason", "Could not open '%s'" % name)
 
446
            self.fail_test(1)
 
447
            return ''
355
448
 
356
449
    def read_and_strip(self, name):
357
 
        lines = open(self.native_file_name(name), "rb").readlines()
 
450
        lines = open(self.glob_file(name), "rb").readlines()
358
451
        result = string.join(map(string.rstrip, lines), "\n")
359
452
        if lines and lines[-1][-1] == '\n':
360
453
            return result + '\n'
364
457
    def fail_test(self, condition, dump_stdio = 1, *args):
365
458
        # If test failed, print the difference        
366
459
        if condition and hasattr(self, 'difference'):            
367
 
            print '-------- all changes caused by last build command ----------'
368
 
            self.difference.pprint()
 
460
            f = StringIO()
 
461
            self.difference.pprint(f)
 
462
            annotation("changes causes by the last build command", f.getvalue())
369
463
            
370
464
        if condition and dump_stdio:
371
465
            self.dump_stdio()
380
474
            elif os.path.exists(path):
381
475
                raise "The path " + path + " already exists and is not directory";
382
476
            shutil.copytree(self.workdir, path)
383
 
                        
384
 
        TestCmd.TestCmd.fail_test(self, condition, *args)
 
477
 
 
478
        if condition:
 
479
            at = TestCmd.caller(traceback.extract_stack(), 0)
 
480
            annotation("stacktrace", at)
 
481
            sys.exit(1)
385
482
        
386
483
    # A number of methods below check expectations with actual difference
387
484
    # between directory trees before and after build.
392
489
    def expect_addition(self, names):        
393
490
        for name in self.adjust_names(names):
394
491
                try:
395
 
                        self.unexpected_difference.added_files.remove(name)
 
492
                        glob_remove(self.unexpected_difference.added_files,name)
396
493
                except:
397
494
                        print "File %s not added as expected" % (name,)
398
495
                        self.fail_test(1)
403
500
    def expect_removal(self, names):
404
501
        for name in self.adjust_names(names):
405
502
                try:
406
 
                        self.unexpected_difference.removed_files.remove(name)
 
503
                        glob_remove(self.unexpected_difference.removed_files,name)
407
504
                except:
408
505
                        print "File %s not removed as expected" % (name,)
409
506
                        self.fail_test(1)
414
511
    def expect_modification(self, names):
415
512
        for name in self.adjust_names(names):
416
513
                try:
417
 
                        self.unexpected_difference.modified_files.remove(name)
 
514
                        glob_remove(self.unexpected_difference.modified_files,name)
418
515
                except:
419
516
                        print "File %s not modified as expected" % (name,)
420
517
                        self.fail_test(1)
436
533
 
437
534
            while filesets:
438
535
                try:
439
 
                    filesets[-1].remove(name)
 
536
                    glob_remove(filesets[-1],name)
440
537
                    break
441
538
                except ValueError:
442
539
                    filesets.pop()
443
540
 
444
541
            if not filesets:
445
 
                print "File %s not touched as expected" % (name,)
 
542
                annotation("reason",
 
543
                           "File %s not touched as expected" % (name,))
446
544
                self.fail_test(1)
447
545
 
448
546
 
458
556
    def expect_nothing(self, names):
459
557
        for name in self.adjust_names(names):
460
558
            if name in self.difference.added_files:
461
 
                print "File %s is added, but no action was expected" % (name,)
 
559
                annotation("reason",
 
560
                           "File %s is added, but no action was expected" % (name,))
462
561
                self.fail_test(1)
463
562
            if name in self.difference.removed_files:
464
 
                print "File %s is removed, but no action was expected" % (name,)
 
563
                annotation("reason",
 
564
                           "File %s is removed, but no action was expected" % (name,))
465
565
                self.fail_test(1)
466
566
                pass
467
567
            if name in self.difference.modified_files:
468
 
                print "File %s is modified, but no action was expected" % (name,)
 
568
                annotation("reason",
 
569
                           "File %s is modified, but no action was expected" % (name,))
469
570
                self.fail_test(1)
470
571
            if name in self.difference.touched_files:
471
 
                print "File %s is touched, but no action was expected" % (name,)
 
572
                annotation("reason",
 
573
                           "File %s is touched, but no action was expected" % (name,))
472
574
                self.fail_test(1)
473
575
 
474
576
    def expect_nothing_more(self):
489
591
           print 'FAILED'
490
592
           print '------- The following changes were unexpected ------- '
491
593
           self.unexpected_difference.pprint()
492
 
           self.fail_test(1)       
493
 
 
494
 
    def expect_content(self, name, content, exact=0):
 
594
           self.fail_test(1)
 
595
 
 
596
    def _expect_line(self, content, expected):
 
597
        expected = expected.strip()
 
598
        lines = content.splitlines()
 
599
        found = 0
 
600
        for line in lines:
 
601
            line = line.strip()
 
602
            if fnmatch.fnmatch(line, expected):
 
603
                found = 1
 
604
                break
 
605
 
 
606
        if not found:
 
607
            print "Did not found expected line in output:"
 
608
            print expected
 
609
            print "The output was:"
 
610
            print content
 
611
            self.fail_test(1)
 
612
 
 
613
    def expect_output_line(self, expected):
 
614
        self._expect_line(self.stdout(), expected)
 
615
 
 
616
    def expect_content_line(self, name, expected):
 
617
        content = self._read_file(name)
 
618
        self._expect_line(content, expected)
 
619
 
 
620
    def _read_file(self, name, exact=0):
495
621
        name = self.adjust_names(name)[0]
 
622
        result = ""
496
623
        try:
497
624
            if exact:
498
 
                actual = self.read(name)
 
625
                result = self.read(name)
499
626
            else:
500
 
                actual = string.replace(self.read_and_strip(name), "\\", "/")
501
 
        except IOError:
 
627
                result = string.replace(self.read_and_strip(name), "\\", "/")
 
628
        except (IOError, IndexError):
502
629
            print "Note: could not open file", name
503
630
            self.fail_test(1)
504
 
 
505
 
        content = string.replace(content, "$toolset", self.toolset)
506
 
 
507
 
        if actual != content:
 
631
        return result
 
632
            
 
633
 
 
634
    def expect_content(self, name, content, exact=0):
 
635
        actual = self._read_file(name, exact)
 
636
        content = string.replace(content, "$toolset", self.toolset+"*")
 
637
 
 
638
        matched = 0
 
639
        if exact:
 
640
            matched = fnmatch.fnmatch(actual,content)
 
641
        else:
 
642
            actual_ = map(lambda x: sorted(x.split()),actual.splitlines())
 
643
            content_ = map(lambda x: sorted(x.split()),content.splitlines())
 
644
            if len(actual_) == len(content_):
 
645
                matched = map(
 
646
                    lambda x,y: map(lambda n,p: fnmatch.fnmatch(n,p),x,y),
 
647
                    actual_, content_ )
 
648
                matched = reduce(
 
649
                    lambda x,y: x and reduce(
 
650
                        lambda a,b: a and b,
 
651
                    y ),
 
652
                    matched )
 
653
 
 
654
        if not matched:
508
655
            print "Expected:\n"
509
656
            print content
510
657
            print "Got:\n"
520
667
            open(a, "w").write(actual)
521
668
            print "DIFFERENCE"
522
669
            if os.system("diff -u " + e + " " + a):
523
 
                print "Unable to compute difference"               
 
670
                print "Unable to compute difference: diff -u %s %s" % (e,a)
524
671
            os.unlink(e)
525
672
            os.unlink(a)    
526
673
        else:
559
706
        pos = string.rfind(name, ".")
560
707
        if pos != -1:
561
708
            suffix = name[pos:]
562
 
            if suffix in [".lib", ".dll"]:
 
709
            if suffix == ".lib":
563
710
                (head, tail) = os.path.split(name)
564
711
                if lib_prefix:
565
712
                    tail = "lib" + tail
566
713
                    result = os.path.join(head, tail)
 
714
            elif suffix == ".dll":
 
715
                (head, tail) = os.path.split(name)
 
716
                if dll_prefix:
 
717
                    tail = "lib" + tail
 
718
                    result = os.path.join(head, tail)
567
719
        return result
568
720
                
569
721
    def adjust_suffix(self, name):
589
741
                names = [names]
590
742
        r = map(self.adjust_lib_name, names)
591
743
        r = map(self.adjust_suffix, r)
592
 
        r = map(lambda x, t=self.toolset: string.replace(x, "$toolset", t), r)
 
744
        r = map(lambda x, t=self.toolset: string.replace(x, "$toolset", t+"*"), r)
593
745
        return r
594
746
 
595
747
    def native_file_name(self, name):
600
752
    # Wait while time is no longer equal to the time last "run_build_system"
601
753
    # call finished.
602
754
    def wait_for_time_change(self):
603
 
        while int(time.time()) < int(self.last_build_time) + 1:
604
 
            time.sleep(0.1)
 
755
        while 1:
 
756
            f = time.time();
 
757
            # In fact, I'm not sure why "+ 2" as opposed to "+ 1" is
 
758
            # needed but empirically, "+ 1" sometimes causes 'touch'
 
759
            # and other functions not to bump file time enough for
 
760
            # rebuild to happen.
 
761
            if math.floor(f) < math.floor(self.last_build_time) + 2:
 
762
                time.sleep(0.1)
 
763
            else:
 
764
                break
 
765
            
605
766
 
606
767
            
607
768
class List: