mirror of
https://git.rtems.org/rtems-tools/
synced 2025-05-14 02:21:32 +08:00
421 lines
14 KiB
Python
Executable File
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)
|