diff --git a/src/beast/SConstruct b/src/beast/SConstruct index 4ace976a43..1513509394 100644 --- a/src/beast/SConstruct +++ b/src/beast/SConstruct @@ -1,160 +1,48 @@ -# Beast scons file -# -#------------------------------------------------------------------------------- +from __future__ import absolute_import, division, print_function, unicode_literals -import ntpath +import copy import os 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 -def print_nv_pair(n, v): - name = ("%s" % n.rjust(10)) - sys.stdout.write("%s \033[94m%s\033[0m\n" % (name, v)) +add_beast_to_path() -# Pretty-print values as a build configuration -def print_build_vars(env,var): - val = env.get(var, '') +from beast.env.AddCommonFlags import add_common_flags +from beast.env.AddUserEnv import add_user_env +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 != '': - name = ("%s" % var.rjust(10)) +VARIANT_DIRECTORIES = { + 'beast': ('bin', 'beast'), + 'modules': ('bin', 'modules'), +} - wrapper = textwrap.TextWrapper() - wrapper.break_long_words = False - wrapper.break_on_hyphens = False - 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: ... -# -# 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: ... -# -# 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) - -#------------------------------------------------------------------------------- +BOOST_LIBRARIES = 'boost_system', +MAIN_PROGRAM_FILE = 'beast/unit_test/tests/main.cpp' +DOTFILE = '~/.scons' 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.VariantDir (os.path.join ('bin', 'modules'), 'modules', duplicate=0) + env = Environment(**working) - # Copy important os environment variables into env - if os.environ.get ('CC', None): - env.Replace (CC = os.environ['CC']) - if os.environ.get ('CXX', None): - 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) + for name, path in VARIANT_DIRECTORIES.items(): + env.VariantDir(os.path.join(*path), name, duplicate=0) + env.Replace(PRINT_CMD_LINE_FUNC=Print.print_cmd_line) + Tests.run_tests(env, MAIN_PROGRAM_FILE, '.', '.test.cpp') main() - diff --git a/src/beast/beast/chrono/basic_seconds_clock.h b/src/beast/beast/chrono/basic_seconds_clock.h index 2e15fdcc42..42db9ab95a 100644 --- a/src/beast/beast/chrono/basic_seconds_clock.h +++ b/src/beast/beast/chrono/basic_seconds_clock.h @@ -22,6 +22,7 @@ #include #include +#include #ifndef BEAST_BASIC_SECONDS_CLOCK_BOOST_WORKAROUND # ifdef _MSC_VER diff --git a/src/beast/beast/config/PlatformConfig.h b/src/beast/beast/config/PlatformConfig.h index 5e94998da3..12af9c889c 100644 --- a/src/beast/beast/config/PlatformConfig.h +++ b/src/beast/beast/config/PlatformConfig.h @@ -101,10 +101,6 @@ #define BEAST_DEBUG 1 #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__ #define BEAST_LITTLE_ENDIAN 1 #else @@ -213,4 +209,3 @@ #define BEAST_FILEANDLINE_ __FILE__ "(" BEAST_PP_STR1_(__LINE__) "): warning:" #endif - diff --git a/src/beast/python/README b/src/beast/python/README new file mode 100644 index 0000000000..494b1cb07d --- /dev/null +++ b/src/beast/python/README @@ -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. \ No newline at end of file diff --git a/src/beast/python/beast/__init__.py b/src/beast/python/beast/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/beast/python/beast/env/AddCommonFlags.py b/src/beast/python/beast/env/AddCommonFlags.py new file mode 100644 index 0000000000..d541262a62 --- /dev/null +++ b/src/beast/python/beast/env/AddCommonFlags.py @@ -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 diff --git a/src/beast/python/beast/env/AddUserEnv.py b/src/beast/python/beast/env/AddUserEnv.py new file mode 100644 index 0000000000..8d5056a708 --- /dev/null +++ b/src/beast/python/beast/env/AddUserEnv.py @@ -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) diff --git a/src/beast/python/beast/env/Print.py b/src/beast/python/beast/env/Print.py new file mode 100644 index 0000000000..163b73bfab --- /dev/null +++ b/src/beast/python/beast/env/Print.py @@ -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() diff --git a/src/beast/python/beast/env/ReadEnvFile.py b/src/beast/python/beast/env/ReadEnvFile.py new file mode 100644 index 0000000000..fce83e2172 --- /dev/null +++ b/src/beast/python/beast/env/ReadEnvFile.py @@ -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 diff --git a/src/beast/python/beast/env/ReadEnvFile_test.py b/src/beast/python/beast/env/ReadEnvFile_test.py new file mode 100644 index 0000000000..7c419b5717 --- /dev/null +++ b/src/beast/python/beast/env/ReadEnvFile_test.py @@ -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"']) diff --git a/src/beast/python/beast/env/Tags.py b/src/beast/python/beast/env/Tags.py new file mode 100644 index 0000000000..6bd0861e75 --- /dev/null +++ b/src/beast/python/beast/env/Tags.py @@ -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) diff --git a/src/beast/python/beast/env/__init__.py b/src/beast/python/beast/env/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/beast/python/beast/platform/GetEnvironment.py b/src/beast/python/beast/platform/GetEnvironment.py new file mode 100644 index 0000000000..89b14f1964 --- /dev/null +++ b/src/beast/python/beast/platform/GetEnvironment.py @@ -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 diff --git a/src/beast/python/beast/platform/Platform.py b/src/beast/python/beast/platform/Platform.py new file mode 100644 index 0000000000..1b16e9f6ac --- /dev/null +++ b/src/beast/python/beast/platform/Platform.py @@ -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() diff --git a/src/beast/python/beast/platform/__init__.py b/src/beast/python/beast/platform/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/beast/python/beast/util/Boost.py b/src/beast/python/beast/util/Boost.py new file mode 100644 index 0000000000..d7faf17af7 --- /dev/null +++ b/src/beast/python/beast/util/Boost.py @@ -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) diff --git a/src/beast/python/beast/util/Dict.py b/src/beast/python/beast/util/Dict.py new file mode 100644 index 0000000000..67ee13369c --- /dev/null +++ b/src/beast/python/beast/util/Dict.py @@ -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)) diff --git a/src/beast/python/beast/util/Dict_test.py b/src/beast/python/beast/util/Dict_test.py new file mode 100644 index 0000000000..690921e229 --- /dev/null +++ b/src/beast/python/beast/util/Dict_test.py @@ -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') diff --git a/src/beast/python/beast/util/Execute.py b/src/beast/python/beast/util/Execute.py new file mode 100644 index 0000000000..65c6adfd1b --- /dev/null +++ b/src/beast/python/beast/util/Execute.py @@ -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) diff --git a/src/beast/python/beast/util/File.py b/src/beast/python/beast/util/File.py new file mode 100644 index 0000000000..b8c99f54b0 --- /dev/null +++ b/src/beast/python/beast/util/File.py @@ -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))) diff --git a/src/beast/python/beast/util/Git.py b/src/beast/python/beast/util/Git.py new file mode 100644 index 0000000000..31cecad47a --- /dev/null +++ b/src/beast/python/beast/util/Git.py @@ -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)) diff --git a/src/beast/python/beast/util/Iter.py b/src/beast/python/beast/util/Iter.py new file mode 100644 index 0000000000..976165cb53 --- /dev/null +++ b/src/beast/python/beast/util/Iter.py @@ -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 diff --git a/src/beast/python/beast/util/String.py b/src/beast/python/beast/util/String.py new file mode 100644 index 0000000000..63d89084fa --- /dev/null +++ b/src/beast/python/beast/util/String.py @@ -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 [] diff --git a/src/beast/python/beast/util/String_test.py b/src/beast/python/beast/util/String_test.py new file mode 100644 index 0000000000..9e88b37be0 --- /dev/null +++ b/src/beast/python/beast/util/String_test.py @@ -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']) diff --git a/src/beast/python/beast/util/Terminal.py b/src/beast/python/beast/util/Terminal.py new file mode 100644 index 0000000000..d5b33c0894 --- /dev/null +++ b/src/beast/python/beast/util/Terminal.py @@ -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)) diff --git a/src/beast/python/beast/util/Tests.py b/src/beast/python/beast/util/Tests.py new file mode 100644 index 0000000000..2ad1986ae9 --- /dev/null +++ b/src/beast/python/beast/util/Tests.py @@ -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) diff --git a/src/beast/python/beast/util/__init__.py b/src/beast/python/beast/util/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/beast/python/clean-python.py b/src/beast/python/clean-python.py new file mode 100755 index 0000000000..1a18804354 --- /dev/null +++ b/src/beast/python/clean-python.py @@ -0,0 +1,5 @@ +#!/bin/bash + +# Remove all the compiled .pyc files at or below this directory. + +find . -name \*.pyc | xargs rm diff --git a/src/beast/python/run-tests.sh b/src/beast/python/run-tests.sh new file mode 100755 index 0000000000..0b181ca7ab --- /dev/null +++ b/src/beast/python/run-tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Run all the beast Python unit tests. + +python -m unittest discover -p \*_test.py