Improvements to scons build for beast.

* Common code extracted to Python directories.
* Read ~/.scons file for scons environment defaults.
* Override scons settings with shell environment variables.
* New "tags" for debug, nodebug, optimize, nooptimize builds.
* Universal platform detection.
* Default value of environment variables set through prefix dictionaries.
* Check for correct Boost value and fail otherwise.
* Extract git describe --tags into a preprocesor variable, -DTIP_BRANCH
* More colors - blue for unchanged defaults, green for changed defaults, red for error.
* Contain unit tests for non-obvious stuff.
* Check to see that boost libraries have been built.
* Right now, we accept both .dylib and .a versions but it'd be easy to enforce .a only.
This commit is contained in:
Tom Ritchford
2014-04-09 15:33:34 -04:00
committed by Vinnie Falco
parent 04ea9ff74c
commit 524f41177c
29 changed files with 701 additions and 150 deletions

View File

@@ -1,160 +1,48 @@
# Beast scons file from __future__ import absolute_import, division, print_function, unicode_literals
#
#-------------------------------------------------------------------------------
import ntpath import copy
import os import os
import sys import sys
import textwrap
#import re
#import json
#import urlparse
#import posixpath
#import string
#import subprocess
#import platform
#import itertools
#------------------------------------------------------------------------------- def add_beast_to_path():
python_home = os.path.join(os.getcwd(), 'python')
if python_home not in sys.path:
sys.path.append(python_home)
# Format a name value pair add_beast_to_path()
def print_nv_pair(n, v):
name = ("%s" % n.rjust(10))
sys.stdout.write("%s \033[94m%s\033[0m\n" % (name, v))
# Pretty-print values as a build configuration from beast.env.AddCommonFlags import add_common_flags
def print_build_vars(env,var): from beast.env.AddUserEnv import add_user_env
val = env.get(var, '') from beast.env import Print
from beast.platform import GetEnvironment
from beast.util import Boost
from beast.util import File
from beast.util import Tests
if val and val != '': VARIANT_DIRECTORIES = {
name = ("%s" % var.rjust(10)) 'beast': ('bin', 'beast'),
'modules': ('bin', 'modules'),
}
wrapper = textwrap.TextWrapper() BOOST_LIBRARIES = 'boost_system',
wrapper.break_long_words = False MAIN_PROGRAM_FILE = 'beast/unit_test/tests/main.cpp'
wrapper.break_on_hyphens = False DOTFILE = '~/.scons'
wrapper.width = 69
if type(val) is str:
lines = wrapper.wrap(val)
else:
lines = wrapper.wrap(" ".join(str(x) for x in val))
for line in lines:
print_nv_pair (name, line)
name = " "
def print_build_config(env):
config_vars = ['CC', 'CXX', 'CFLAGS', 'CCFLAGS', 'CPPFLAGS',
'CXXFLAGS', 'LIBPATH', 'LINKFLAGS', 'LIBS', 'BOOST_HOME']
sys.stdout.write("\nConfiguration:\n")
for var in config_vars:
print_build_vars(env,var)
print
def print_cmd_line(s, target, src, env):
target = (''.join([str(x) for x in target]))
source = (''.join([str(x) for x in src]))
name = target
print ' \033[94m' + name + '\033[0m'
#-------------------------------------------------------------------------------
# Returns the list of libraries needed by the test source file. This is
# accomplished by scanning the source file for a special comment line
# with this format, which must match exactly:
#
# // LIBS: <name>...
#
# path = path to source file
#
def get_libs(path):
prefix = '// LIBS:'
with open(path, 'rb') as f:
for line in f:
line = line.strip()
if line.startswith(prefix):
items = line.split(prefix, 1)[1].strip()
return [x.strip() for x in items.split(' ')]
# Returns the list of source modules needed by the test source file. This
#
# // MODULES: <module>...
#
# path = path to source file
#
def get_mods(path):
prefix = '// MODULES:'
with open(path, 'rb') as f:
for line in f:
line = line.strip()
if line.startswith(prefix):
items = line.split(prefix, 1)[1].strip()
items = [os.path.normpath(os.path.join(
os.path.dirname(path), x.strip())) for
x in items.split(' ')]
return items
# Build a stand alone executable that runs
# all the test suites in one source file
#
def build_test(env,path):
libs = get_libs(path)
mods = get_mods(path)
bin = os.path.basename(os.path.splitext(path)[0])
bin = os.path.join ("bin", bin)
srcs = ['beast/unit_test/tests/main.cpp']
srcs.append (path)
if mods:
srcs.extend (mods)
# All paths get normalized here, so we can use posix
# forward slashes for everything including on Windows
srcs = [os.path.normpath(os.path.join ('bin', x)) for x in srcs]
objs = [os.path.splitext(x)[0]+'.o' for x in srcs]
env_ = env
if libs:
env_.Append(LIBS = libs)
env_.Program (bin, srcs)
#-------------------------------------------------------------------------------
def main(): def main():
env = Environment() File.validate_libraries(Boost.LIBPATH, BOOST_LIBRARIES)
defaults = GetEnvironment.get_environment(ARGUMENTS)
working = copy.deepcopy(defaults)
add_common_flags(defaults)
env['PRINT_CMD_LINE_FUNC'] = print_cmd_line add_user_env(working, DOTFILE)
add_common_flags(working)
Print.print_build_config(working, defaults)
env.VariantDir (os.path.join ('bin', 'beast'), 'beast', duplicate=0) env = Environment(**working)
env.VariantDir (os.path.join ('bin', 'modules'), 'modules', duplicate=0)
# Copy important os environment variables into env for name, path in VARIANT_DIRECTORIES.items():
if os.environ.get ('CC', None): env.VariantDir(os.path.join(*path), name, duplicate=0)
env.Replace (CC = os.environ['CC']) env.Replace(PRINT_CMD_LINE_FUNC=Print.print_cmd_line)
if os.environ.get ('CXX', None): Tests.run_tests(env, MAIN_PROGRAM_FILE, '.', '.test.cpp')
env.Replace (CXX = os.environ['CXX'])
if os.environ.get ('PATH', None):
env.Replace (PATH = os.environ['PATH'])
# Set up boost variables
home = os.environ.get("BOOST_HOME", None)
if home is not None:
env.Prepend (CPPPATH = home)
env.Append (LIBPATH = os.path.join (home, 'stage', 'lib'))
# Set up flags
env.Append(CXXFLAGS = [
'-std=c++11',
'-frtti',
'-O3',
'-fno-strict-aliasing',
'-g'
])
for root, dirs, files in os.walk('.'):
for path in files:
path = os.path.join(root,path)
if (path.endswith(".test.cpp")):
build_test(env,path)
print_build_config (env)
main() main()

View File

@@ -22,6 +22,7 @@
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <vector>
#ifndef BEAST_BASIC_SECONDS_CLOCK_BOOST_WORKAROUND #ifndef BEAST_BASIC_SECONDS_CLOCK_BOOST_WORKAROUND
# ifdef _MSC_VER # ifdef _MSC_VER

View File

@@ -101,10 +101,6 @@
#define BEAST_DEBUG 1 #define BEAST_DEBUG 1
#endif #endif
#if ! (defined (DEBUG) || defined (_DEBUG) || defined (NDEBUG) || defined (_NDEBUG))
// #warning "Neither NDEBUG or DEBUG has been defined - you should set one of these to make it clear whether this is a release build,"
#endif
#ifdef __LITTLE_ENDIAN__ #ifdef __LITTLE_ENDIAN__
#define BEAST_LITTLE_ENDIAN 1 #define BEAST_LITTLE_ENDIAN 1
#else #else
@@ -213,4 +209,3 @@
#define BEAST_FILEANDLINE_ __FILE__ "(" BEAST_PP_STR1_(__LINE__) "): warning:" #define BEAST_FILEANDLINE_ __FILE__ "(" BEAST_PP_STR1_(__LINE__) "): warning:"
#endif #endif

10
src/beast/python/README Normal file
View File

@@ -0,0 +1,10 @@
Python code for the beast and scons.
Scripts you can run from this directory.
./run-tests.sh
Runs the unit tests.
./clean-python.sh
If you remove or rename any Python files, you should run this script to
prevent old .pyc files from hiding bugs.

View File

View File

@@ -0,0 +1,11 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from beast.util import Boost
from beast.util import Git
def add_common_flags(env):
git_flag = '-DTIP_BRANCH="%s"' % Git.describe()
env['CPPFLAGS'] = '%s %s' % (env['CPPFLAGS'], git_flag)
env['CPPPATH'].insert(0, Boost.CPPPATH)
env['LIBPATH'].append(Boost.LIBPATH)
env['BOOST_HOME'] = Boost.CPPPATH

View File

@@ -0,0 +1,38 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import os
import shlex
from beast.env.ReadEnvFile import read_env_file
from beast.util.String import is_string
from beast.util.Terminal import warn
_BAD_VARS_ERROR = """
the following variables appearing in %s were not understood:
%s"""
def add_user_env(env, dotfile, print=print):
df = os.path.expanduser(dotfile)
try:
with open(df, 'r') as f:
dotvars = read_env_file(f.read())
except IOError:
if os.path.exists(df):
warn("Dotfile %s exists but can't be read." % dotfile, print)
dotvars = {}
bad_names = []
for name, value in dotvars.items():
if name in env:
if is_string(env[name]):
env[name] = value
else:
env[name] = shlex.split(value)
else:
bad_names.append(name)
if bad_names:
error = _BAD_VARS_ERROR % (dotfile, '\n '.join(bad_names))
warn(error, print)
for name, default in env.items():
env[name] = os.environ.get(name, default)

41
src/beast/python/beast/env/Print.py vendored Normal file
View File

@@ -0,0 +1,41 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import textwrap
from beast.util import String
from beast.util import Terminal
FIELD_WIDTH = 10
LINE_WIDTH = 69
EMPTY_NAME = ' ' * FIELD_WIDTH
TEXT_WRAPPER = textwrap.TextWrapper(
break_long_words=False,
break_on_hyphens=False,
width=LINE_WIDTH,
)
DISPLAY_EMPTY_ENVS = True
def print_build_vars(name, value, same, print=print):
"""Pretty-print values as a build configuration."""
name = '%s' % name.rjust(FIELD_WIDTH)
color = Terminal.blue if same else Terminal.green
for line in TEXT_WRAPPER.wrap(String.stringify(value, ' ')):
print(' '.join([name, color(line)]))
name = EMPTY_NAME
def print_cmd_line(s, target, source, env):
print(EMPTY_NAME + Terminal.blue(String.stringify(target)))
def print_build_config(env, original, print=print):
print('\nConfiguration:')
for name, value in env.items():
if value or DISPLAY_EMPTY_ENVS:
same = (value == original[name])
if not same:
print('"%s" != "%s"' % (value, original[name]))
print_build_vars(name, value, same, print=print)
print()

View File

@@ -0,0 +1,34 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import json
import re
from beast.util import String
from beast.util.Terminal import warn
ENV_LINE_MATCH = re.compile(r'(?: export \s+)? \s* ([^=\s]*) \s* = (.*)',
re.VERBOSE)
def read_env_file(data, print=print):
try:
return json.loads(data)
except ValueError:
pass
bad_lines = []
results = {}
for number, raw_line in enumerate(data.splitlines()):
line = String.remove_comment(raw_line).strip()
if line:
match = ENV_LINE_MATCH.match(line)
if match:
name, value = match.groups()
results[name.strip()] = String.remove_quotes(value.strip())
else:
bad_lines.append([number, raw_line])
if bad_lines:
warn("Didn't understand the following environment file lines:", print)
for number, line in bad_lines:
print('%d. >>> %s' % (number + 1, line))
return results

View File

@@ -0,0 +1,51 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from unittest import TestCase
from beast.env.ReadEnvFile import read_env_file
from beast.util import Terminal
Terminal.CAN_CHANGE_COLOR = False
JSON = """
{
"FOO": "foo",
"BAR": "bar bar bar",
"CPPFLAGS": "-std=c++11 -frtti -fno-strict-aliasing -DWOMBAT"
}"""
ENV = """
# An env file.
FOO=foo
export BAR="bar bar bar"
CPPFLAGS=-std=c++11 -frtti -fno-strict-aliasing -DWOMBAT
# export BAZ=baz should be ignored.
"""
RESULT = {
'FOO': 'foo',
'BAR': 'bar bar bar',
'CPPFLAGS': '-std=c++11 -frtti -fno-strict-aliasing -DWOMBAT',
}
BAD_ENV = ENV + """
This line isn't right.
NO SPACES IN NAMES="valid value"
"""
class test_ReadEnvFile(TestCase):
def test_read_json(self):
self.assertEqual(read_env_file(JSON), RESULT)
def test_read_env(self):
self.assertEqual(read_env_file(ENV), RESULT)
def test_read_env_error(self):
errors = []
self.assertEqual(read_env_file(BAD_ENV, errors.append), RESULT)
self.assertEqual(errors, [
"WARNING: Didn't understand the following environment file lines:",
"11. >>> This line isn't right.",
'12. >>> NO SPACES IN NAMES="valid value"'])

17
src/beast/python/beast/env/Tags.py vendored Normal file
View File

@@ -0,0 +1,17 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from beast.util.Terminal import warn
_TAGS = frozenset(['debug', 'optimize'])
def _to_tag(name, value):
return '%s%s' % ('' if value else 'no', name)
def get_tags(arguments, print=print):
result = {}
bad_tags = set(arguments) - _TAGS
if bad_tags:
warn("don't understand tags " + ' '.join(bad_tags), print=print)
debug = result.get('debug', True)
optimize = result.get('optimize', not debug)
return _to_tag('debug', debug), _to_tag('optimize', optimize)

View File

View File

@@ -0,0 +1,59 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from beast.platform import Platform
from beast.util import Dict
from beast.env import Tags
_DEFAULTS = {
'': {
'BOOST_HOME': None,
'CC': 'gcc',
'CCFLAGS': None,
'CFLAGS': None,
'CXX': 'g++',
'CPPFLAGS': '-std=c++11 -frtti -fno-strict-aliasing',
'CPPPATH': [],
'LIBPATH': [],
'LIBS': [],
'LINKFLAGS': '',
},
'Darwin': {
'CC': 'clang',
'CXX': 'clang++',
'CPPFLAGS': '-x c++ -stdlib=libc++ -std=c++11 -frtti',
'LINKFLAGS': '-stdlib=libc++',
},
'FreeBSD': {
'CC': 'gcc46',
'CXX': 'g++46',
'CCFLAGS': '-Wl,-rpath=/usr/local/lib/gcc46',
'LINKFLAGS': '-Wl,-rpath=/usr/local/lib/gcc46',
'LIBS': ['kvm'],
},
# TODO: specific flags for Windows, Linux platforms.
}
TAGS = {
'debug': {
'CPPFLAGS': '-g -DDEBUG'
},
'optimize': {
'CPPFLAGS': '-O3',
},
'nooptimize': {
'CPPFLAGS': '-O0',
}
}
def get_environment(arguments):
tags = Tags.get_tags(arguments)
env = Dict.compose_prefix_dicts(Platform.PLATFORM, _DEFAULTS)
for tag in tags or []:
for k, v in TAGS.get(tag, {}).items():
env[k] = '%s %s' % (env[k], v)
return env

View File

@@ -0,0 +1,27 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import platform
def _get_platform_string():
system = platform.system()
parts = [system]
linux = system == 'Linux'
if linux:
flavor, version, _ = platform.linux_distribution()
# Arch still has issues with the platform module
parts[0] = flavor.capitalize() or 'Archlinux'
parts.extend(version.split('.'))
elif system == 'Darwin':
ten, major, minor = platform.mac_ver()[0].split('.')
parts.extend([ten, major, minor])
elif system == 'Windows':
release, version, csd, ptype = platform.win32_ver()
parts.extend([release, version, csd, ptype])
elif system == 'FreeBSD':
# No other variables to pass with FreeBSD that Python provides and I could find
pass
else:
raise Exception("Don't understand how to build for platform " + system)
return '.'.join(parts), linux
PLATFORM, IS_LINUX = _get_platform_string()

View File

@@ -0,0 +1,61 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import os
import re
ROOT_ENV_VARIABLE = 'BOOST_ROOT'
MINIMUM_VERSION = 1, 55, 0
VERSION_FILE = 'boost', 'version.hpp'
LIBRARY_PATH_SEGMENT = 'stage', 'lib'
VERSION_MATCHER = re.compile(r'#define\s+BOOST_VERSION\s+(\d+)')
CANT_OPEN_VERSION_FILE_ERROR = """Unable to open boost version file %s.
You have set the environment variable BOOST_ROOT to be %s.
Please check to make sure that this points to a valid installation of boost."""
CANT_UNDERSTAND_VERSION_ERROR = (
"Didn't understand version string '%s' from file %s'")
VERSION_TOO_OLD_ERROR = ('Your version of boost, %s, is older than the minimum '
'required, %s.')
def _text(major, minor, release):
return '%d.%02d.%02d' % (major, minor, release)
def _raw_boost_path():
try:
path = os.environ[ROOT_ENV_VARIABLE]
if path:
return os.path.normpath(path)
except KeyError:
pass
raise KeyError('%s environment variable is not set.' % ROOT_ENV_VARIABLE)
def _get_version_number(path):
version_file = os.path.join(path, *VERSION_FILE)
try:
with open(version_file) as f:
for line in f:
match = VERSION_MATCHER.match(line)
if match:
version = match.group(1)
try:
return int(version)
except ValueError:
raise Exception(CANT_UNDERSTAND_VERSION_ERROR %
(version, version_file))
except IOError:
raise Exception(CANT_OPEN_VERSION_FILE_ERROR % (version_file, path))
def _validate_version(v):
version = v // 100000, (v // 100) % 100, v % 100
if version < MINIMUM_VERSION:
raise Exception(VERSION_TOO_OLD_ERROR % (
_text(*version), _text(*MINIMUM_VERSION)))
def _boost_path():
path = _raw_boost_path()
_validate_version(_get_version_number(path))
return path
CPPPATH = _boost_path()
LIBPATH = os.path.join(CPPPATH, *LIBRARY_PATH_SEGMENT)

View File

@@ -0,0 +1,17 @@
from __future__ import absolute_import, division, print_function, unicode_literals
def compose(*dicts):
result = {}
for d in dicts:
result.update(**d)
return result
def get_items_with_prefix(key, mapping):
"""Get all elements from the mapping whose keys are a prefix of the given
key, sorted by increasing key length."""
for k, v in sorted(mapping.items()):
if key.startswith(k):
yield v
def compose_prefix_dicts(key, mapping):
return compose(*get_items_with_prefix(key, mapping))

View File

@@ -0,0 +1,56 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from unittest import TestCase
from beast.util import Dict
DICT = {
'': {
'foo': 'foo-default',
'bar': 'bar-default',
},
'Darwin': {
'foo': 'foo-darwin',
'baz': 'baz-darwin',
},
'Darwin.10.8': {
'foo': 'foo-darwin-10.8',
'bing': 'bing-darwin-10.8',
},
}
class test_Dict(TestCase):
def computeMapValue(self, config, key):
return Dict.compose(*Dict.get_items_with_prefix(config, DICT))[key]
def assertMapValue(self, config, key, result):
self.assertEquals(self.computeMapValue(config, key), result)
def testDefault1(self):
self.assertMapValue('', 'foo', 'foo-default')
def testDefault2(self):
self.assertMapValue('Darwin.10.8', 'bar', 'bar-default')
def testPrefix1(self):
self.assertMapValue('Darwin', 'foo', 'foo-darwin')
def testPrefix2(self):
self.assertMapValue('Darwin.10.8', 'foo', 'foo-darwin-10.8')
def testPrefix3(self):
self.assertMapValue('Darwin', 'baz', 'baz-darwin')
def testPrefix4(self):
self.assertMapValue('Darwin.10.8', 'bing', 'bing-darwin-10.8')
def testFailure1(self):
self.assertRaises(KeyError, self.computeMapValue, '', 'baz')
def testFailure2(self):
self.assertRaises(KeyError, self.computeMapValue, '', 'bing')
def testFailure2(self):
self.assertRaises(KeyError, self.computeMapValue, 'Darwin', 'bing')

View File

@@ -0,0 +1,14 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import subprocess
from beast.util import String
def execute(args, include_errors=True, **kwds):
"""Execute a shell command and return the value. If args is a string,
it's split on spaces - if some of your arguments contain spaces, args should
instead be a list of arguments."""
if String.is_string(args):
args = args.split()
stderr = subprocess.STDOUT if include_errors else None
return subprocess.check_output(args, stderr=stderr, **kwds)

View File

@@ -0,0 +1,42 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from beast.util import String
import os
LIBRARY_PATTERNS = 'lib%s.a', 'lib%s.dylib'
def first_fields_after_prefix(filename, prefix):
with open(filename, 'r') as f:
return String.first_fields_after_prefix(prefix, f)
def find_files_with_suffix(base, suffix):
for parent, _, files in os.walk(base):
for path in files:
path = os.path.join(parent, path)
if path.endswith(suffix):
yield os.path.normpath(path)
def child_files(parent, files):
return [os.path.normpath(os.path.join(parent, f)) for f in files]
def sibling_files(path, files):
return child_files(os.path.dirname(path), files)
def replace_extension(file, ext):
return os.path.splitext(file)[0] + ext
def validate_libraries(path, libraries):
bad = []
for lib in libraries:
found = False
for pat in LIBRARY_PATTERNS:
libfile = os.path.join(path, pat % lib)
if os.path.isfile(libfile):
found = True
break
if not found:
bad.append(libfile)
if bad:
libs = 'library' if len(bad) == 1 else 'libraries'
raise Exception('Missing %s: %s' % (libs, ', '.join(bad)))

View File

@@ -0,0 +1,9 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import os
from beast.util import Execute
from beast.util import String
def describe(**kwds):
return String.single_line(Execute.execute('git describe --tags', **kwds))

View File

@@ -0,0 +1,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals
def first(condition, sequence):
for i in sequence:
result = condition(i)
if result:
return result

View File

@@ -0,0 +1,60 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import functools
from beast.util import Iter
from beast.util.Terminal import warn
def is_string(s):
"""Is s a string? - in either Python 2.x or 3.x."""
return isinstance(s, (str, unicode))
def stringify(item, joiner=''):
"""If item is not a string, stringify its members and join them."""
try:
len(item)
except:
return str(item)
if not item or is_string(item):
return item or ''
else:
return joiner.join(str(i) for i in item)
def single_line(line, report_errors=True, joiner='+'):
"""Force a string to be a single line with no carriage returns, and report
a warning if there was more than one line."""
lines = line.strip().splitlines()
if report_errors and len(lines) > 1:
print('multiline result:', lines)
return joiner.join(lines)
# Copied from
# https://github.com/lerugray/pickett/blob/master/pickett/ParseScript.py
def remove_comment(line):
"""Remove trailing comments from one line."""
start = 0
while True:
loc = line.find('#', start)
if loc == -1:
return line.replace('\\#', '#')
elif not (loc and line[loc - 1] == '\\'):
return line[:loc].replace('\\#', '#')
start = loc + 1
def remove_quotes(line, quote='"', print=print):
if not line.startswith(quote):
return line
if line.endswith(quote):
return line[1:-1]
warn('line started with %s but didn\'t end with one:' % quote, print)
print(line)
return line[1:]
def fields_after_prefix(prefix, line):
line = line.strip()
return line.startswith(prefix) and line[len(prefix):].split()
def first_fields_after_prefix(prefix, sequence):
condition = functools.partial(fields_after_prefix, prefix)
return Iter.first(condition, sequence) or []

View File

@@ -0,0 +1,36 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from unittest import TestCase
from beast.util import String
from beast.util import Terminal
Terminal.CAN_CHANGE_COLOR = False
class String_test(TestCase):
def test_comments(self):
self.assertEqual(String.remove_comment(''), '')
self.assertEqual(String.remove_comment('#'), '')
self.assertEqual(String.remove_comment('# a comment'), '')
self.assertEqual(String.remove_comment('hello # a comment'), 'hello ')
self.assertEqual(String.remove_comment(
r'hello \# not a comment # a comment'),
'hello # not a comment ')
def test_remove_quotes(self):
errors = []
self.assertEqual(String.remove_quotes('hello', print=errors.append),
'hello')
self.assertEqual(String.remove_quotes('"hello"', print=errors.append),
'hello')
self.assertEqual(String.remove_quotes('hello"', print=errors.append),
'hello"')
self.assertEqual(errors, [])
def test_remove_quotes_error(self):
errors = []
self.assertEqual(String.remove_quotes('"hello', print=errors.append),
'hello')
self.assertEqual(errors,
['WARNING: line started with " but didn\'t end with one:',
'"hello'])

View File

@@ -0,0 +1,36 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import sys
from beast.platform.Platform import PLATFORM
# See https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python
CAN_CHANGE_COLOR = (
hasattr(sys.stderr, "isatty")
and sys.stderr.isatty()
and not PLATFORM.startswith('Windows'))
# See https://en.wikipedia.org/wiki/ANSI_escape_code
RED = 91
GREEN = 92
BLUE = 94
def add_mode(text, *modes):
if CAN_CHANGE_COLOR:
modes = ';'.join(str(m) for m in modes)
return '\033[%sm%s\033[0m' % (modes, text)
else:
return text
def blue(text):
return add_mode(text, BLUE)
def green(text):
return add_mode(text, GREEN)
def red(text):
return add_mode(text, RED)
def warn(text, print=print):
print('%s %s' % (red('WARNING:'), text))

View File

@@ -0,0 +1,31 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import os
from beast.util import File
LIBS_PREFIX = '// LIBS:'
MODS_PREFIX = '// MODULES:'
def build_executable(env, path, main_program_file):
"""Build a stand alone executable that runs
all the test suites in one source file."""
libs = File.first_fields_after_prefix(path, LIBS_PREFIX)
source_modules = File.first_fields_after_prefix(path, MODS_PREFIX)
source_modules = File.sibling_files(path, source_modules)
bin = os.path.basename(os.path.splitext(path)[0])
bin = os.path.join('bin', bin)
# All paths get normalized here, so we can use posix
# forward slashes for everything including on Windows
srcs = File.child_files('bin', [main_program_file, path] + source_modules)
objs = [File.replace_extension(f, '.o') for f in srcs]
if libs:
env.Append(LIBS=libs) # DANGER: will append the file over and over.
env.Program(bin, srcs)
def run_tests(env, main_program_file, root, suffix):
root = os.path.normpath(root)
for path in File.find_files_with_suffix(root, suffix):
build_executable(env, path, main_program_file)

View File

View File

@@ -0,0 +1,5 @@
#!/bin/bash
# Remove all the compiled .pyc files at or below this directory.
find . -name \*.pyc | xargs rm

5
src/beast/python/run-tests.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
# Run all the beast Python unit tests.
python -m unittest discover -p \*_test.py