summaryrefslogtreecommitdiff
path: root/doc/asciidoc/tests/testasciidoc.py
diff options
context:
space:
mode:
Diffstat (limited to 'doc/asciidoc/tests/testasciidoc.py')
-rwxr-xr-xdoc/asciidoc/tests/testasciidoc.py420
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)