source: rtems-tools/doc/asciidoc/tests/testasciidoc.py @ f91e023

4.104.115
Last change on this file since f91e023 was f91e023, checked in by Chris Johns <chrisj@…>, on 02/17/14 at 07:04:46

Add the documentation.

  • Property mode set to 100755
File size: 13.7 KB
Line 
1#!/usr/bin/env python
2
3USAGE = '''Usage: testasciidoc.py [OPTIONS] COMMAND
4
5Run AsciiDoc conformance tests specified in configuration FILE.
6
7Commands:
8  list                          List tests
9  run [NUMBER] [BACKEND]        Execute tests
10  update [NUMBER] [BACKEND]     Regenerate and update test data
11
12Options:
13  -f, --conf-file=CONF_FILE
14        Use configuration file CONF_FILE (default configuration file is
15        testasciidoc.conf in testasciidoc.py directory)
16  --force
17        Update all test data overwriting existing data'''
18
19
20__version__ = '0.1.1'
21__copyright__ = 'Copyright (C) 2009 Stuart Rackham'
22
23
24import os, sys, re, difflib
25
26if sys.platform[:4] == 'java':
27    # Jython cStringIO is more compatible with CPython StringIO.
28    import cStringIO as StringIO
29else:
30    import StringIO
31
32import asciidocapi
33
34
35BACKENDS = ('html4','xhtml11','docbook','wordpress','html5')    # Default backends.
36BACKEND_EXT = {'html4':'.html', 'xhtml11':'.html', 'docbook':'.xml',
37        'wordpress':'.html','slidy':'.html','html5':'.html'}
38
39
40def iif(condition, iftrue, iffalse=None):
41    """
42    Immediate if c.f. ternary ?: operator.
43    False value defaults to '' if the true value is a string.
44    False value defaults to 0 if the true value is a number.
45    """
46    if iffalse is None:
47        if isinstance(iftrue, basestring):
48            iffalse = ''
49        if type(iftrue) in (int, float):
50            iffalse = 0
51    if condition:
52        return iftrue
53    else:
54        return iffalse
55
56def message(msg=''):
57    print >>sys.stderr, msg
58
59def strip_end(lines):
60    """
61    Strip blank strings from the end of list of strings.
62    """
63    for i in range(len(lines)-1,-1,-1):
64        if not lines[i]:
65            del lines[i]
66        else:
67            break
68
69def normalize_data(lines):
70    """
71    Strip comments and trailing blank strings from lines.
72    """
73    result = [ s for s in lines if not s.startswith('#') ]
74    strip_end(result)
75    return result
76
77
78class AsciiDocTest(object):
79
80    def __init__(self):
81        self.number = None      # Test number (1..).
82        self.name = ''          # Optional test name.
83        self.title = ''         # Optional test name.
84        self.description = []   # List of lines followoing title.
85        self.source = None      # AsciiDoc test source file name.
86        self.options = []
87        self.attributes = {}
88        self.backends = BACKENDS
89        self.datadir = None     # Where output files are stored.
90        self.disabled = False
91
92    def backend_filename(self, backend):
93        """
94        Return the path name of the backend  output file that is generated from
95        the test name and output file type.
96        """
97        return '%s-%s%s' % (
98                os.path.normpath(os.path.join(self.datadir, self.name)),
99                backend,
100                BACKEND_EXT[backend])
101
102    def parse(self, lines, confdir, datadir):
103        """
104        Parse conf file test section from list of text lines.
105        """
106        self.__init__()
107        self.confdir = confdir
108        self.datadir = datadir
109        lines = Lines(lines)
110        while not lines.eol():
111            l = lines.read_until(r'^%')
112            if l:
113                if not l[0].startswith('%'):
114                    if l[0][0] == '!':
115                        self.disabled = True
116                        self.title = l[0][1:]
117                    else:
118                        self.title = l[0]
119                    self.description = l[1:]
120                    continue
121                reo = re.match(r'^%\s*(?P<directive>[\w_-]+)', l[0])
122                if not reo:
123                    raise (ValueError, 'illegal directive: %s' % l[0])
124                directive = reo.groupdict()['directive']
125                data = normalize_data(l[1:])
126                if directive == 'source':
127                    if data:
128                        self.source = os.path.normpath(os.path.join(
129                                self.confdir, os.path.normpath(data[0])))
130                elif directive == 'options':
131                    self.options = eval(' '.join(data))
132                    for i,v in enumerate(self.options):
133                        if isinstance(v, basestring):
134                            self.options[i] = (v,None)
135                elif directive == 'attributes':
136                    self.attributes = eval(' '.join(data))
137                elif directive == 'backends':
138                    self.backends = eval(' '.join(data))
139                elif directive == 'name':
140                    self.name = data[0].strip()
141                else:
142                    raise (ValueError, 'illegal directive: %s' % l[0])
143        if not self.title:
144            self.title = self.source
145        if not self.name:
146            self.name = os.path.basename(os.path.splitext(self.source)[0])
147
148    def is_missing(self, backend):
149        """
150        Returns True if there is no output test data file for backend.
151        """
152        return not os.path.isfile(self.backend_filename(backend))
153
154    def is_missing_or_outdated(self, backend):
155        """
156        Returns True if the output test data file is missing or out of date.
157        """
158        return self.is_missing(backend) or (
159               os.path.getmtime(self.source)
160               > os.path.getmtime(self.backend_filename(backend)))
161
162    def get_expected(self, backend):
163        """
164        Return expected test data output for backend.
165        """
166        f = open(self.backend_filename(backend))
167        try:
168            result = f.readlines()
169            # Strip line terminators.
170            result = [ s.rstrip() for s in result ]
171        finally:
172            f.close()
173        return result
174
175    def generate_expected(self, backend):
176        """
177        Generate and return test data output for backend.
178        """
179        asciidoc = asciidocapi.AsciiDocAPI()
180        asciidoc.options.values = self.options
181        asciidoc.attributes = self.attributes
182        infile = self.source
183        outfile = StringIO.StringIO()
184        asciidoc.execute(infile, outfile, backend)
185        return outfile.getvalue().splitlines()
186
187    def update_expected(self, backend):
188        """
189        Generate and write backend data.
190        """
191        lines = self.generate_expected(backend)
192        if not os.path.isdir(self.datadir):
193            print('CREATING: %s' % self.datadir)
194            os.mkdir(self.datadir)
195        f = open(self.backend_filename(backend),'w+')
196        try:
197            print('WRITING: %s' % f.name)
198            f.writelines([ s + os.linesep for s in lines])
199        finally:
200            f.close()
201
202    def update(self, backend=None, force=False):
203        """
204        Regenerate and update expected test data outputs.
205        """
206        if backend is None:
207            backends = self.backends
208        else:
209            backends = [backend]
210        for backend in backends:
211            if force or self.is_missing_or_outdated(backend):
212                self.update_expected(backend)
213
214    def run(self, backend=None):
215        """
216        Execute test.
217        Return True if test passes.
218        """
219        if backend is None:
220            backends = self.backends
221        else:
222            backends = [backend]
223        result = True   # Assume success.
224        self.passed = self.failed = self.skipped = 0
225        print('%d: %s' % (self.number, self.title))
226        if self.source and os.path.isfile(self.source):
227            print('SOURCE: asciidoc: %s' % self.source)
228            for backend in backends:
229                fromfile = self.backend_filename(backend)
230                if not self.is_missing(backend):
231                    expected = self.get_expected(backend)
232                    strip_end(expected)
233                    got = self.generate_expected(backend)
234                    strip_end(got)
235                    lines = []
236                    for line in difflib.unified_diff(got, expected, n=0):
237                        lines.append(line)
238                    if lines:
239                        result = False
240                        self.failed +=1
241                        lines = lines[3:]
242                        print('FAILED: %s: %s' % (backend, fromfile))
243                        message('+++ %s' % fromfile)
244                        message('--- got')
245                        for line in lines:
246                            message(line)
247                        message()
248                    else:
249                        self.passed += 1
250                        print('PASSED: %s: %s' % (backend, fromfile))
251                else:
252                    self.skipped += 1
253                    print('SKIPPED: %s: %s' % (backend, fromfile))
254        else:
255            self.skipped += len(backends)
256            if self.source:
257                msg = 'MISSING: %s' % self.source
258            else:
259                msg = 'NO ASCIIDOC SOURCE FILE SPECIFIED'
260            print(msg)
261        print('')
262        return result
263
264
265class AsciiDocTests(object):
266
267    def __init__(self, conffile):
268        """
269        Parse configuration file.
270        """
271        self.conffile = os.path.normpath(conffile)
272        # All file names are relative to configuration file directory.
273        self.confdir = os.path.dirname(self.conffile)
274        self.datadir = self.confdir # Default expected files directory.
275        self.tests = []             # List of parsed AsciiDocTest objects.
276        self.globals = {}
277        f = open(self.conffile)
278        try:
279            lines = Lines(f.readlines())
280        finally:
281            f.close()
282        first = True
283        while not lines.eol():
284            s = lines.read_until(r'^%+$')
285            s = [ l for l in s if l]    # Drop blank lines.
286            # Must be at least one non-blank line in addition to delimiter.
287            if len(s) > 1:
288                # Optional globals precede all tests.
289                if first and re.match(r'^%\s*globals$',s[0]):
290                    self.globals = eval(' '.join(normalize_data(s[1:])))
291                    if 'datadir' in self.globals:
292                        self.datadir = os.path.join(
293                                self.confdir,
294                                os.path.normpath(self.globals['datadir']))
295                else:
296                    test = AsciiDocTest()
297                    test.parse(s[1:], self.confdir, self.datadir)
298                    self.tests.append(test)
299                    test.number = len(self.tests)
300                first = False
301
302    def run(self, number=None, backend=None):
303        """
304        Run all tests.
305        If number is specified run test number (1..).
306        """
307        self.passed = self.failed = self.skipped = 0
308        for test in self.tests:
309            if (not test.disabled or number) and (not number or number == test.number) and (not backend or backend in test.backends):
310                test.run(backend)
311                self.passed += test.passed
312                self.failed += test.failed
313                self.skipped += test.skipped
314        if self.passed > 0:
315            print('TOTAL PASSED:  %s' % self.passed)
316        if self.failed > 0:
317            print('TOTAL FAILED:  %s' % self.failed)
318        if self.skipped > 0:
319            print('TOTAL SKIPPED: %s' % self.skipped)
320
321    def update(self, number=None, backend=None, force=False):
322        """
323        Regenerate expected test data and update configuratio file.
324        """
325        for test in self.tests:
326            if (not test.disabled or number) and (not number or number == test.number):
327                test.update(backend, force=force)
328
329    def list(self):
330        """
331        Lists tests to stdout.
332        """
333        for test in self.tests:
334            print '%d: %s%s' % (test.number, iif(test.disabled,'!'), test.title)
335
336
337class Lines(list):
338    """
339    A list of strings.
340    Adds eol() and read_until() to list type.
341    """
342
343    def __init__(self, lines):
344        super(Lines, self).__init__()
345        self.extend([s.rstrip() for s in lines])
346        self.pos = 0
347
348    def eol(self):
349        return self.pos >= len(self)
350
351    def read_until(self, regexp):
352        """
353        Return a list of lines from current position up until the next line
354        matching regexp.
355        Advance position to matching line.
356        """
357        result = []
358        if not self.eol():
359            result.append(self[self.pos])
360            self.pos += 1
361        while not self.eol():
362            if re.match(regexp, self[self.pos]):
363                break
364            result.append(self[self.pos])
365            self.pos += 1
366        return result
367
368
369def usage(msg=None):
370    if msg:
371        message(msg + '\n')
372    message(USAGE)
373
374
375if __name__ == '__main__':
376    # Process command line options.
377    import getopt
378    try:
379        opts,args = getopt.getopt(sys.argv[1:], 'f:', ['force'])
380    except getopt.GetoptError:
381        usage('illegal command options')
382        sys.exit(1)
383    if len(args) == 0:
384        usage()
385        sys.exit(1)
386    conffile = os.path.join(os.path.dirname(sys.argv[0]), 'testasciidoc.conf')
387    force = False
388    for o,v in opts:
389        if o == '--force':
390            force = True
391        if o in ('-f','--conf-file'):
392            conffile = v
393    if not os.path.isfile(conffile):
394        message('missing CONF_FILE: %s' % conffile)
395        sys.exit(1)
396    tests = AsciiDocTests(conffile)
397    cmd = args[0]
398    number = None
399    backend = None
400    for arg in args[1:3]:
401        try:
402            number = int(arg)
403        except ValueError:
404            backend = arg
405    if backend and backend not in BACKENDS:
406        message('illegal BACKEND: %s' % backend)
407        sys.exit(1)
408    if number is not None and  number not in range(1, len(tests.tests)+1):
409        message('illegal test NUMBER: %d' % number)
410        sys.exit(1)
411    if cmd == 'run':
412        tests.run(number, backend)
413        if tests.failed:
414            exit(1)
415    elif cmd == 'update':
416        tests.update(number, backend, force=force)
417    elif cmd == 'list':
418        tests.list()
419    else:
420        usage('illegal COMMAND: %s' % cmd)
Note: See TracBrowser for help on using the repository browser.