rtems-tools/doc/asciidoc/tests/testasciidoc.py
2014-02-17 18:04:46 +11:00

421 lines
14 KiB
Python
Executable File

#!/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)