summaryrefslogtreecommitdiff
path: root/tools/litre/cplusplus.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/litre/cplusplus.py')
-rw-r--r--tools/litre/cplusplus.py810
1 files changed, 810 insertions, 0 deletions
diff --git a/tools/litre/cplusplus.py b/tools/litre/cplusplus.py
new file mode 100644
index 0000000000..c48e0678ed
--- /dev/null
+++ b/tools/litre/cplusplus.py
@@ -0,0 +1,810 @@
+# Copyright David Abrahams 2004.
+# Copyright Daniel Wallin 2006.
+# Distributed under the Boost
+# Software License, Version 1.0. (See accompanying
+# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+import os
+import tempfile
+import litre
+import re
+import sys
+import traceback
+
+# Thanks to Jean Brouwers for this snippet
+def _caller(up=0):
+ '''Get file name, line number, function name and
+ source text of the caller's caller as 4-tuple:
+ (file, line, func, text).
+
+ The optional argument 'up' allows retrieval of
+ a caller further back up into the call stack.
+
+ Note, the source text may be None and function
+ name may be '?' in the returned result. In
+ Python 2.3+ the file name may be an absolute
+ path.
+ '''
+ try: # just get a few frames'
+ f = traceback.extract_stack(limit=up+2)
+ if f:
+ return f[0]
+ except:
+ pass
+ # running with psyco?
+ return ('', 0, '', None)
+
+class Example:
+ closed = False
+ in_emph = None
+
+ def __init__(self, node, section, line_offset, line_hash = '#'):
+ # A list of text fragments comprising the Example. Start with a #line
+ # directive
+ self.section = section
+ self.line_hash = line_hash
+ self.node = node
+ self.body = []
+ self.line_offset = line_offset
+ self._number_of_prefixes = 0
+
+ self.emphasized = [] # indices of text strings that have been
+ # emphasized. These are generally expected to be
+ # invalid C++ and will need special treatment
+
+ def begin_emphasis(self):
+ self.in_emph = len(self.body)
+
+ def end_emphasis(self):
+ self.emphasized.append( (self.in_emph, len(self.body)) )
+
+ def append(self, s):
+ self.append_raw(self._make_line(s))
+
+ def prepend(self, s):
+ self.prepend_raw(self._make_line(s))
+
+ def append_raw(self, s):
+ self.body.append(s)
+
+ def prepend_raw(self, s):
+ self.body.insert(0,s)
+ self.emphasized = [ (x[0]+1,x[1]+1) for x in self.emphasized ]
+ self._number_of_prefixes += 1
+
+ def replace(self, s1, s2):
+ self.body = [x.replace(s1,s2) for x in self.body]
+
+ def sub(self, pattern, repl, count = 1, flags = re.MULTILINE):
+ pat = re.compile(pattern, flags)
+ for i,txt in enumerate(self.body):
+ if count > 0:
+ x, subs = pat.subn(repl, txt, count)
+ self.body[i] = x
+ count -= subs
+
+ def wrap(self, s1, s2):
+ self.append_raw(self._make_line(s2))
+ self.prepend_raw(self._make_line(s1, offset = -s1.count('\n')))
+
+ def replace_emphasis(self, s, index = 0):
+ """replace the index'th emphasized text with s"""
+ e = self.emphasized[index]
+ self.body[e[0]:e[1]] = [s]
+ del self.emphasized[index]
+
+ elipsis = re.compile('^([ \t]*)([.][.][.][ \t]*)$', re.MULTILINE)
+
+ def __str__(self):
+ # Comment out any remaining emphasized sections
+ b = [self.elipsis.sub(r'\1// \2', s) for s in self.body]
+ emph = self.emphasized
+ emph.reverse()
+ for e in emph:
+ b.insert(e[1], ' */')
+ b.insert(e[0], '/* ')
+ emph.reverse()
+
+ # Add initial #line
+ b.insert(
+ self._number_of_prefixes,
+ self._line_directive(self.node.line, self.node.source)
+ )
+
+ # Add trailing newline to avoid warnings
+ b.append('\n')
+ return ''.join(b)
+
+ def __repr__(self):
+ return "Example: " + repr(str(self))
+
+ def raw(self):
+ return ''.join(self.body)
+
+ def _make_line(self, s, offset = 0):
+ c = _caller(2)[1::-1]
+ offset -= s.count('\n')
+ return '\n%s%s\n' % (self._line_directive(offset = offset, *c), s.strip('\n'))
+
+ def _line_directive(self, line, source, offset = None):
+ if self.line_hash is None:
+ return '\n'
+
+ if offset is None:
+ offset = self.line_offset
+
+ if line is None or line <= -offset:
+ line = 1
+ else:
+ line += offset
+
+ if source is None:
+ return '%sline %d\n' % (self.line_hash, line)
+ else:
+ return '%sline %d "%s"\n' % (self.line_hash, line, source)
+
+
+def syscmd(
+ cmd
+ , expect_error = False
+ , input = None
+ , max_output_lines = None
+ ):
+
+ # On windows close() returns the exit code, on *nix it doesn't so
+ # we need to use popen2.Popen4 instead.
+ if sys.platform == 'win32':
+ stdin, stdout_stderr = os.popen4(cmd)
+ if input: stdin.write(input)
+ stdin.close()
+
+ out = stdout_stderr.read()
+ status = stdout_stderr.close()
+ else:
+ import popen2
+ process = popen2.Popen4(cmd)
+ if input: process.tochild.write(input)
+ out = process.fromchild.read()
+ status = process.wait()
+
+ if max_output_lines is not None:
+ out = '\n'.join(out.split('\n')[:max_output_lines])
+
+ if expect_error:
+ status = not status
+
+ if status:
+ print
+ print '========== offending command ==========='
+ print cmd
+ print '------------ stdout/stderr -------------'
+ print expect_error and 'Error expected, but none seen' or out
+ elif expect_error > 1:
+ print
+ print '------ Output of Expected Error --------'
+ print out
+ print '----------------------------------------'
+
+ sys.stdout.flush()
+
+ return (status,out)
+
+
+def expand_vars(path):
+ if os.name == 'nt':
+ re_env = re.compile(r'%\w+%')
+ return re_env.sub(
+ lambda m: os.environ.get( m.group(0)[1:-1] )
+ , path
+ )
+ else:
+ return os.path.expandvars(path)
+
+def remove_directory_and_contents(path):
+ for root, dirs, files in os.walk(path, topdown=False):
+ for name in files:
+ os.remove(os.path.join(root, name))
+ for name in dirs:
+ os.rmdir(os.path.join(root, name))
+ os.rmdir(path)
+
+class BuildResult:
+ def __init__(self, path):
+ self.path = path
+
+ def __repr__(self):
+ return self.path
+
+ def __del__(self):
+ remove_directory_and_contents(self.path)
+
+class CPlusPlusTranslator(litre.LitreTranslator):
+
+ _exposed_attrs = ['compile', 'test', 'ignore', 'match_stdout', 'stack', 'config'
+ , 'example', 'prefix', 'preprocessors', 'litre_directory',
+ 'litre_translator', 'includes', 'build', 'jam_prefix',
+ 'run_python']
+
+ last_run_output = ''
+
+ """Attributes that will be made available to litre code"""
+
+ def __init__(self, document, config):
+ litre.LitreTranslator.__init__(self, document, config)
+ self.in_literal = False
+ self.in_table = True
+ self.preprocessors = []
+ self.stack = []
+ self.example = None
+ self.prefix = []
+ self.includes = config.includes
+ self.litre_directory = os.path.split(__file__)[0]
+ self.config = config
+ self.litre_translator = self
+ self.line_offset = 0
+ self.last_source = None
+ self.jam_prefix = []
+
+ self.globals = { 'test_literals_in_tables' : False }
+ for m in self._exposed_attrs:
+ self.globals[m] = getattr(self, m)
+
+ self.examples = {}
+ self.current_section = None
+
+ #
+ # Stuff for use by docutils writer framework
+ #
+ def visit_emphasis(self, node):
+ if self.in_literal:
+ self.example.begin_emphasis()
+
+ def depart_emphasis(self, node):
+ if self.in_literal:
+ self.example.end_emphasis()
+
+ def visit_section(self, node):
+ self.current_section = node['ids'][0]
+
+ def visit_literal_block(self, node):
+ if node.source is None:
+ node.source = self.last_source
+ self.last_source = node.source
+
+ # create a new example
+ self.example = Example(node, self.current_section, line_offset = self.line_offset, line_hash = self.config.line_hash)
+
+ self.stack.append(self.example)
+
+ self.in_literal = True
+
+ def depart_literal_block(self, node):
+ self.in_literal = False
+
+ def visit_literal(self, node):
+ if self.in_table and self.globals['test_literals_in_tables']:
+ self.visit_literal_block(node)
+ else:
+ litre.LitreTranslator.visit_literal(self,node)
+
+ def depart_literal(self, node):
+ if self.in_table and self.globals['test_literals_in_tables']:
+ self.depart_literal_block(node)
+ else:
+ litre.LitreTranslator.depart_literal(self,node)
+
+ def visit_table(self,node):
+ self.in_table = True
+ litre.LitreTranslator.visit_table(self,node)
+
+ def depart_table(self,node):
+ self.in_table = False
+ litre.LitreTranslator.depart_table(self,node)
+
+ def visit_Text(self, node):
+ if self.in_literal:
+ self.example.append_raw(node.astext())
+
+ def depart_document(self, node):
+ self.write_examples()
+
+ #
+ # Private stuff
+ #
+
+ def handled(self, n = 1):
+ r = self.stack[-n:]
+ del self.stack[-n:]
+ return r
+
+ def _execute(self, code):
+ """Override of litre._execute; sets up variable context before
+ evaluating code
+ """
+ self.globals['example'] = self.example
+ eval(code, self.globals)
+
+ #
+ # Stuff for use by embedded python code
+ #
+
+ def match_stdout(self, expected = None):
+
+ if expected is None:
+ expected = self.example.raw()
+ self.handled()
+
+ if not re.search(expected, self.last_run_output, re.MULTILINE):
+ #if self.last_run_output.strip('\n') != expected.strip('\n'):
+ print 'output failed to match example'
+ print '-------- Actual Output -------------'
+ print repr(self.last_run_output)
+ print '-------- Expected Output -----------'
+ print repr(expected)
+ print '------------------------------------'
+ sys.stdout.flush()
+
+ def ignore(self, n = 1):
+ if n == 'all':
+ n = len(self.stack)
+ return self.handled(n)
+
+ def wrap(self, n, s1, s2):
+ self.stack[-1].append(s2)
+ self.stack[-n].prepend(s1)
+
+
+ def compile(
+ self
+ , howmany = 1
+ , pop = -1
+ , expect_error = False
+ , extension = '.o'
+ , options = ['-c']
+ , built_handler = lambda built_file: None
+ , source_file = None
+ , source_suffix = '.cpp'
+ # C-style comments by default; handles C++ and YACC
+ , make_comment = lambda text: '/*\n%s\n*/' % text
+ , built_file = None
+ , command = None
+ ):
+ """
+ Compile examples on the stack, whose topmost item is the last example
+ seen but not yet handled so far.
+
+ :howmany: How many of the topmost examples on the stack to compile.
+ You can pass a number, or 'all' to indicate that all examples should
+ be compiled.
+
+ :pop: How many of the topmost examples to discard. By default, all of
+ the examples that are compiled are discarded.
+
+ :expect_error: Whether a compilation error is to be expected. Any value
+ > 1 will cause the expected diagnostic's text to be dumped for
+ diagnostic purposes. It's common to expect an error but see a
+ completely unrelated one because of bugs in the example (you can get
+ this behavior for all examples by setting show_expected_error_output
+ in your config).
+
+ :extension: The extension of the file to build (set to .exe for
+ run)
+
+ :options: Compiler flags
+
+ :built_file: A path to use for the built file. By default, a temp
+ filename is conjured up
+
+ :built_handler: A function that's called with the name of the built file
+ upon success.
+
+ :source_file: The full name of the source file to write
+
+ :source_suffix: If source_file is None, the suffix to use for the source file
+
+ :make_comment: A function that transforms text into an appropriate comment.
+
+ :command: A function that is passed (includes, opts, target, source), where
+ opts is a string representing compiler options, target is the name of
+ the file to build, and source is the name of the file into which the
+ example code is written. By default, the function formats
+ litre.config.compiler with its argument tuple.
+ """
+
+ # Grab one example by default
+ if howmany == 'all':
+ howmany = len(self.stack)
+
+ source = '\n'.join(
+ self.prefix
+ + [str(x) for x in self.stack[-howmany:]]
+ )
+
+ source = reduce(lambda s, f: f(s), self.preprocessors, source)
+
+ if pop:
+ if pop < 0:
+ pop = howmany
+ del self.stack[-pop:]
+
+ if len(self.stack):
+ self.example = self.stack[-1]
+
+ cpp = self._source_file_path(source_file, source_suffix)
+
+ if built_file is None:
+ built_file = self._output_file_path(source_file, extension)
+
+ opts = ' '.join(options)
+
+ includes = ' '.join(['-I%s' % d for d in self.includes])
+ if not command:
+ command = self.config.compiler
+
+ if type(command) == str:
+ command = lambda i, o, t, s, c = command: c % (i, o, t, s)
+
+ cmd = command(includes, opts, expand_vars(built_file), expand_vars(cpp))
+
+ if expect_error and self.config.show_expected_error_output:
+ expect_error += 1
+
+
+ comment_cmd = command(includes, opts, built_file, os.path.basename(cpp))
+ comment = make_comment(config.comment_text(comment_cmd, expect_error))
+
+ self._write_source(cpp, '\n'.join([comment, source]))
+
+ #print 'wrote in', cpp
+ #print 'trying command', cmd
+
+ status, output = syscmd(cmd, expect_error)
+
+ if status or expect_error > 1:
+ print
+ if expect_error and expect_error < 2:
+ print 'Compilation failure expected, but none seen'
+ print '------------ begin offending source ------------'
+ print open(cpp).read()
+ print '------------ end offending source ------------'
+
+ if self.config.save_cpp:
+ print 'saved in', repr(cpp)
+ else:
+ self._remove_source(cpp)
+
+ sys.stdout.flush()
+ else:
+ print '.',
+ sys.stdout.flush()
+ built_handler(built_file)
+
+ self._remove_source(cpp)
+
+ try:
+ self._unlink(built_file)
+ except:
+ if not expect_error:
+ print 'failed to unlink', built_file
+
+ return status
+
+ def test(
+ self
+ , rule = 'run'
+ , howmany = 1
+ , pop = -1
+ , expect_error = False
+ , requirements = ''
+ , input = ''
+ ):
+
+ # Grab one example by default
+ if howmany == 'all':
+ howmany = len(self.stack)
+
+ source = '\n'.join(
+ self.prefix
+ + [str(x) for x in self.stack[-howmany:]]
+ )
+
+ source = reduce(lambda s, f: f(s), self.preprocessors, source)
+
+ id = self.example.section
+ if not id:
+ id = 'top-level'
+
+ if not self.examples.has_key(self.example.section):
+ self.examples[id] = [(rule, source)]
+ else:
+ self.examples[id].append((rule, source))
+
+ if pop:
+ if pop < 0:
+ pop = howmany
+ del self.stack[-pop:]
+
+ if len(self.stack):
+ self.example = self.stack[-1]
+
+ def write_examples(self):
+ jam = open(os.path.join(self.config.dump_dir, 'Jamfile.v2'), 'w')
+
+ jam.write('''
+import testing ;
+
+''')
+
+ for id,examples in self.examples.items():
+ for i in range(len(examples)):
+ cpp = '%s%d.cpp' % (id, i)
+
+ jam.write('%s %s ;\n' % (examples[i][0], cpp))
+
+ outfile = os.path.join(self.config.dump_dir, cpp)
+ print cpp,
+ try:
+ if open(outfile, 'r').read() == examples[i][1]:
+ print ' .. skip'
+ continue
+ except:
+ pass
+
+ open(outfile, 'w').write(examples[i][1])
+ print ' .. written'
+
+ jam.close()
+
+ def build(
+ self
+ , howmany = 1
+ , pop = -1
+ , source_file = 'example.cpp'
+ , expect_error = False
+ , target_rule = 'obj'
+ , requirements = ''
+ , input = ''
+ , output = 'example_output'
+ ):
+
+ # Grab one example by default
+ if howmany == 'all':
+ howmany = len(self.stack)
+
+ source = '\n'.join(
+ self.prefix
+ + [str(x) for x in self.stack[-howmany:]]
+ )
+
+ source = reduce(lambda s, f: f(s), self.preprocessors, source)
+
+ if pop:
+ if pop < 0:
+ pop = howmany
+ del self.stack[-pop:]
+
+ if len(self.stack):
+ self.example = self.stack[-1]
+
+ dir = tempfile.mkdtemp()
+ cpp = os.path.join(dir, source_file)
+ self._write_source(cpp, source)
+ self._write_jamfile(
+ dir
+ , target_rule = target_rule
+ , requirements = requirements
+ , input = input
+ , output = output
+ )
+
+ cmd = 'bjam'
+ if self.config.bjam_options:
+ cmd += ' %s' % self.config.bjam_options
+
+ os.chdir(dir)
+ status, output = syscmd(cmd, expect_error)
+
+ if status or expect_error > 1:
+ print
+ if expect_error and expect_error < 2:
+ print 'Compilation failure expected, but none seen'
+ print '------------ begin offending source ------------'
+ print open(cpp).read()
+ print '------------ begin offending Jamfile -----------'
+ print open(os.path.join(dir, 'Jamroot')).read()
+ print '------------ end offending Jamfile -------------'
+
+ sys.stdout.flush()
+ else:
+ print '.',
+ sys.stdout.flush()
+
+ if status: return None
+ else: return BuildResult(dir)
+
+ def _write_jamfile(self, path, target_rule, requirements, input, output):
+ jamfile = open(os.path.join(path, 'Jamroot'), 'w')
+ contents = r"""
+import modules ;
+
+BOOST_ROOT = [ modules.peek : BOOST_ROOT ] ;
+use-project /boost : $(BOOST_ROOT) ;
+
+%s
+
+%s %s
+ : example.cpp %s
+ : <include>.
+ %s
+ %s
+ ;
+ """ % (
+ '\n'.join(self.jam_prefix)
+ , target_rule
+ , output
+ , input
+ , ' '.join(['<include>%s' % d for d in self.includes])
+ , requirements
+ )
+
+ jamfile.write(contents)
+
+ def run_python(
+ self
+ , howmany = 1
+ , pop = -1
+ , module_path = []
+ , expect_error = False
+ ):
+ # Grab one example by default
+ if howmany == 'all':
+ howmany = len(self.stack)
+
+ if module_path == None: module_path = []
+
+ if isinstance(module_path, BuildResult) or type(module_path) == str:
+ module_path = [module_path]
+
+ module_path = map(lambda p: str(p), module_path)
+
+ source = '\n'.join(
+ self.prefix
+ + [str(x) for x in self.stack[-howmany:]]
+ )
+
+ if pop:
+ if pop < 0:
+ pop = howmany
+ del self.stack[-pop:]
+
+ if len(self.stack):
+ self.example = self.stack[-1]
+
+ r = re.compile(r'^(>>>|\.\.\.) (.*)$', re.MULTILINE)
+ source = r.sub(r'\2', source)
+ py = self._source_file_path(source_file = None, source_suffix = 'py')
+ open(py, 'w').write(source)
+
+ old_path = os.getenv('PYTHONPATH')
+ if old_path == None:
+ pythonpath = ':'.join(module_path)
+ old_path = ''
+ else:
+ pythonpath = old_path + ':%s' % ':'.join(module_path)
+
+ os.putenv('PYTHONPATH', pythonpath)
+ status, output = syscmd('python %s' % py)
+
+ if status or expect_error > 1:
+ print
+ if expect_error and expect_error < 2:
+ print 'Compilation failure expected, but none seen'
+ print '------------ begin offending source ------------'
+ print open(py).read()
+ print '------------ end offending Jamfile -------------'
+
+ sys.stdout.flush()
+ else:
+ print '.',
+ sys.stdout.flush()
+
+ self.last_run_output = output
+ os.putenv('PYTHONPATH', old_path)
+ self._unlink(py)
+
+ def _write_source(self, filename, contents):
+ open(filename,'w').write(contents)
+
+ def _remove_source(self, source_path):
+ os.unlink(source_path)
+
+ def _source_file_path(self, source_file, source_suffix):
+ if source_file is None:
+ cpp = tempfile.mktemp(suffix=source_suffix)
+ else:
+ cpp = os.path.join(tempfile.gettempdir(), source_file)
+ return cpp
+
+ def _output_file_path(self, source_file, extension):
+ return tempfile.mktemp(suffix=extension)
+
+ def _unlink(self, file):
+ file = expand_vars(file)
+ if os.path.exists(file):
+ os.unlink(file)
+
+ def _launch(self, exe, stdin = None):
+ status, output = syscmd(exe, input = stdin)
+ self.last_run_output = output
+
+ def run_(self, howmany = 1, stdin = None, **kw):
+ new_kw = { 'options':[], 'extension':'.exe' }
+ new_kw.update(kw)
+
+ self.compile(
+ howmany
+ , built_handler = lambda exe: self._launch(exe, stdin = stdin)
+ , **new_kw
+ )
+
+ def astext(self):
+ return ""
+ return '\n\n ---------------- Unhandled Fragment ------------ \n\n'.join(
+ [''] # generates a leading announcement
+ + [ unicode(s) for s in self.stack]
+ )
+
+class DumpTranslator(CPlusPlusTranslator):
+ example_index = 1
+
+ def _source_file_path(self, source_file, source_suffix):
+ if source_file is None:
+ source_file = 'example%s%s' % (self.example_index, source_suffix)
+ self.example_index += 1
+
+ cpp = os.path.join(config.dump_dir, source_file)
+ return cpp
+
+ def _output_file_path(self, source_file, extension):
+ chapter = os.path.basename(config.dump_dir)
+ return '%%TEMP%%\metaprogram-%s-example%s%s' \
+ % ( chapter, self.example_index - 1, extension)
+
+ def _remove_source(self, source_path):
+ pass
+
+
+class WorkaroundTranslator(DumpTranslator):
+ """Translator used to test/dump workaround examples for vc6 and vc7. Just
+ like a DumpTranslator except that we leave existing files alone.
+
+ Warning: not sensitive to changes in .rst source!! If you change the actual
+ examples in source files you will have to move the example files out of the
+ way and regenerate them, then re-incorporate the workarounds.
+ """
+ def _write_source(self, filename, contents):
+ if not os.path.exists(filename):
+ DumpTranslator._write_source(self, filename, contents)
+
+class Config:
+ save_cpp = False
+ line_hash = '#'
+ show_expected_error_output = False
+ max_output_lines = None
+
+class Writer(litre.Writer):
+ translator = CPlusPlusTranslator
+
+ def __init__(
+ self
+ , config
+ ):
+ litre.Writer.__init__(self)
+ self._config = Config()
+ defaults = Config.__dict__
+
+ # update config elements
+ self._config.__dict__.update(config.__dict__)
+# dict([i for i in config.__dict__.items()
+# if i[0] in config.__all__]))
+