diff options
Diffstat (limited to 'doc/asciidoc/tests/testasciidoc.py')
-rwxr-xr-x | doc/asciidoc/tests/testasciidoc.py | 420 |
1 files changed, 420 insertions, 0 deletions
diff --git a/doc/asciidoc/tests/testasciidoc.py b/doc/asciidoc/tests/testasciidoc.py new file mode 100755 index 0000000..679ad35 --- /dev/null +++ b/doc/asciidoc/tests/testasciidoc.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python + +USAGE = '''Usage: testasciidoc.py [OPTIONS] COMMAND + +Run AsciiDoc conformance tests specified in configuration FILE. + +Commands: + list List tests + run [NUMBER] [BACKEND] Execute tests + update [NUMBER] [BACKEND] Regenerate and update test data + +Options: + -f, --conf-file=CONF_FILE + Use configuration file CONF_FILE (default configuration file is + testasciidoc.conf in testasciidoc.py directory) + --force + Update all test data overwriting existing data''' + + +__version__ = '0.1.1' +__copyright__ = 'Copyright (C) 2009 Stuart Rackham' + + +import os, sys, re, difflib + +if sys.platform[:4] == 'java': + # Jython cStringIO is more compatible with CPython StringIO. + import cStringIO as StringIO +else: + import StringIO + +import asciidocapi + + +BACKENDS = ('html4','xhtml11','docbook','wordpress','html5') # Default backends. +BACKEND_EXT = {'html4':'.html', 'xhtml11':'.html', 'docbook':'.xml', + 'wordpress':'.html','slidy':'.html','html5':'.html'} + + +def iif(condition, iftrue, iffalse=None): + """ + Immediate if c.f. ternary ?: operator. + False value defaults to '' if the true value is a string. + False value defaults to 0 if the true value is a number. + """ + if iffalse is None: + if isinstance(iftrue, basestring): + iffalse = '' + if type(iftrue) in (int, float): + iffalse = 0 + if condition: + return iftrue + else: + return iffalse + +def message(msg=''): + print >>sys.stderr, msg + +def strip_end(lines): + """ + Strip blank strings from the end of list of strings. + """ + for i in range(len(lines)-1,-1,-1): + if not lines[i]: + del lines[i] + else: + break + +def normalize_data(lines): + """ + Strip comments and trailing blank strings from lines. + """ + result = [ s for s in lines if not s.startswith('#') ] + strip_end(result) + return result + + +class AsciiDocTest(object): + + def __init__(self): + self.number = None # Test number (1..). + self.name = '' # Optional test name. + self.title = '' # Optional test name. + self.description = [] # List of lines followoing title. + self.source = None # AsciiDoc test source file name. + self.options = [] + self.attributes = {} + self.backends = BACKENDS + self.datadir = None # Where output files are stored. + self.disabled = False + + def backend_filename(self, backend): + """ + Return the path name of the backend output file that is generated from + the test name and output file type. + """ + return '%s-%s%s' % ( + os.path.normpath(os.path.join(self.datadir, self.name)), + backend, + BACKEND_EXT[backend]) + + def parse(self, lines, confdir, datadir): + """ + Parse conf file test section from list of text lines. + """ + self.__init__() + self.confdir = confdir + self.datadir = datadir + lines = Lines(lines) + while not lines.eol(): + l = lines.read_until(r'^%') + if l: + if not l[0].startswith('%'): + if l[0][0] == '!': + self.disabled = True + self.title = l[0][1:] + else: + self.title = l[0] + self.description = l[1:] + continue + reo = re.match(r'^%\s*(?P<directive>[\w_-]+)', l[0]) + if not reo: + raise (ValueError, 'illegal directive: %s' % l[0]) + directive = reo.groupdict()['directive'] + data = normalize_data(l[1:]) + if directive == 'source': + if data: + self.source = os.path.normpath(os.path.join( + self.confdir, os.path.normpath(data[0]))) + elif directive == 'options': + self.options = eval(' '.join(data)) + for i,v in enumerate(self.options): + if isinstance(v, basestring): + self.options[i] = (v,None) + elif directive == 'attributes': + self.attributes = eval(' '.join(data)) + elif directive == 'backends': + self.backends = eval(' '.join(data)) + elif directive == 'name': + self.name = data[0].strip() + else: + raise (ValueError, 'illegal directive: %s' % l[0]) + if not self.title: + self.title = self.source + if not self.name: + self.name = os.path.basename(os.path.splitext(self.source)[0]) + + def is_missing(self, backend): + """ + Returns True if there is no output test data file for backend. + """ + return not os.path.isfile(self.backend_filename(backend)) + + def is_missing_or_outdated(self, backend): + """ + Returns True if the output test data file is missing or out of date. + """ + return self.is_missing(backend) or ( + os.path.getmtime(self.source) + > os.path.getmtime(self.backend_filename(backend))) + + def get_expected(self, backend): + """ + Return expected test data output for backend. + """ + f = open(self.backend_filename(backend)) + try: + result = f.readlines() + # Strip line terminators. + result = [ s.rstrip() for s in result ] + finally: + f.close() + return result + + def generate_expected(self, backend): + """ + Generate and return test data output for backend. + """ + asciidoc = asciidocapi.AsciiDocAPI() + asciidoc.options.values = self.options + asciidoc.attributes = self.attributes + infile = self.source + outfile = StringIO.StringIO() + asciidoc.execute(infile, outfile, backend) + return outfile.getvalue().splitlines() + + def update_expected(self, backend): + """ + Generate and write backend data. + """ + lines = self.generate_expected(backend) + if not os.path.isdir(self.datadir): + print('CREATING: %s' % self.datadir) + os.mkdir(self.datadir) + f = open(self.backend_filename(backend),'w+') + try: + print('WRITING: %s' % f.name) + f.writelines([ s + os.linesep for s in lines]) + finally: + f.close() + + def update(self, backend=None, force=False): + """ + Regenerate and update expected test data outputs. + """ + if backend is None: + backends = self.backends + else: + backends = [backend] + for backend in backends: + if force or self.is_missing_or_outdated(backend): + self.update_expected(backend) + + def run(self, backend=None): + """ + Execute test. + Return True if test passes. + """ + if backend is None: + backends = self.backends + else: + backends = [backend] + result = True # Assume success. + self.passed = self.failed = self.skipped = 0 + print('%d: %s' % (self.number, self.title)) + if self.source and os.path.isfile(self.source): + print('SOURCE: asciidoc: %s' % self.source) + for backend in backends: + fromfile = self.backend_filename(backend) + if not self.is_missing(backend): + expected = self.get_expected(backend) + strip_end(expected) + got = self.generate_expected(backend) + strip_end(got) + lines = [] + for line in difflib.unified_diff(got, expected, n=0): + lines.append(line) + if lines: + result = False + self.failed +=1 + lines = lines[3:] + print('FAILED: %s: %s' % (backend, fromfile)) + message('+++ %s' % fromfile) + message('--- got') + for line in lines: + message(line) + message() + else: + self.passed += 1 + print('PASSED: %s: %s' % (backend, fromfile)) + else: + self.skipped += 1 + print('SKIPPED: %s: %s' % (backend, fromfile)) + else: + self.skipped += len(backends) + if self.source: + msg = 'MISSING: %s' % self.source + else: + msg = 'NO ASCIIDOC SOURCE FILE SPECIFIED' + print(msg) + print('') + return result + + +class AsciiDocTests(object): + + def __init__(self, conffile): + """ + Parse configuration file. + """ + self.conffile = os.path.normpath(conffile) + # All file names are relative to configuration file directory. + self.confdir = os.path.dirname(self.conffile) + self.datadir = self.confdir # Default expected files directory. + self.tests = [] # List of parsed AsciiDocTest objects. + self.globals = {} + f = open(self.conffile) + try: + lines = Lines(f.readlines()) + finally: + f.close() + first = True + while not lines.eol(): + s = lines.read_until(r'^%+$') + s = [ l for l in s if l] # Drop blank lines. + # Must be at least one non-blank line in addition to delimiter. + if len(s) > 1: + # Optional globals precede all tests. + if first and re.match(r'^%\s*globals$',s[0]): + self.globals = eval(' '.join(normalize_data(s[1:]))) + if 'datadir' in self.globals: + self.datadir = os.path.join( + self.confdir, + os.path.normpath(self.globals['datadir'])) + else: + test = AsciiDocTest() + test.parse(s[1:], self.confdir, self.datadir) + self.tests.append(test) + test.number = len(self.tests) + first = False + + def run(self, number=None, backend=None): + """ + Run all tests. + If number is specified run test number (1..). + """ + self.passed = self.failed = self.skipped = 0 + for test in self.tests: + if (not test.disabled or number) and (not number or number == test.number) and (not backend or backend in test.backends): + test.run(backend) + self.passed += test.passed + self.failed += test.failed + self.skipped += test.skipped + if self.passed > 0: + print('TOTAL PASSED: %s' % self.passed) + if self.failed > 0: + print('TOTAL FAILED: %s' % self.failed) + if self.skipped > 0: + print('TOTAL SKIPPED: %s' % self.skipped) + + def update(self, number=None, backend=None, force=False): + """ + Regenerate expected test data and update configuratio file. + """ + for test in self.tests: + if (not test.disabled or number) and (not number or number == test.number): + test.update(backend, force=force) + + def list(self): + """ + Lists tests to stdout. + """ + for test in self.tests: + print '%d: %s%s' % (test.number, iif(test.disabled,'!'), test.title) + + +class Lines(list): + """ + A list of strings. + Adds eol() and read_until() to list type. + """ + + def __init__(self, lines): + super(Lines, self).__init__() + self.extend([s.rstrip() for s in lines]) + self.pos = 0 + + def eol(self): + return self.pos >= len(self) + + def read_until(self, regexp): + """ + Return a list of lines from current position up until the next line + matching regexp. + Advance position to matching line. + """ + result = [] + if not self.eol(): + result.append(self[self.pos]) + self.pos += 1 + while not self.eol(): + if re.match(regexp, self[self.pos]): + break + result.append(self[self.pos]) + self.pos += 1 + return result + + +def usage(msg=None): + if msg: + message(msg + '\n') + message(USAGE) + + +if __name__ == '__main__': + # Process command line options. + import getopt + try: + opts,args = getopt.getopt(sys.argv[1:], 'f:', ['force']) + except getopt.GetoptError: + usage('illegal command options') + sys.exit(1) + if len(args) == 0: + usage() + sys.exit(1) + conffile = os.path.join(os.path.dirname(sys.argv[0]), 'testasciidoc.conf') + force = False + for o,v in opts: + if o == '--force': + force = True + if o in ('-f','--conf-file'): + conffile = v + if not os.path.isfile(conffile): + message('missing CONF_FILE: %s' % conffile) + sys.exit(1) + tests = AsciiDocTests(conffile) + cmd = args[0] + number = None + backend = None + for arg in args[1:3]: + try: + number = int(arg) + except ValueError: + backend = arg + if backend and backend not in BACKENDS: + message('illegal BACKEND: %s' % backend) + sys.exit(1) + if number is not None and number not in range(1, len(tests.tests)+1): + message('illegal test NUMBER: %d' % number) + sys.exit(1) + if cmd == 'run': + tests.run(number, backend) + if tests.failed: + exit(1) + elif cmd == 'update': + tests.update(number, backend, force=force) + elif cmd == 'list': + tests.list() + else: + usage('illegal COMMAND: %s' % cmd) |