diff --git a/Jenkinsfile b/Jenkinsfile index 1ace2a79..a7e7eb3a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,119 +1,120 @@ pipeline { parameters {string(defaultValue: '', description: 'api-token', name: 'API_TOKEN') string(defaultValue: '', description: 'Token for readthedocs', name: 'RTD_TOKEN') string(defaultValue: '', description: 'buildable phid', name: 'BUILD_TARGET_PHID') string(defaultValue: '', description: 'Commit id', name: 'COMMIT_ID') string(defaultValue: '', description: 'Diff id', name: 'DIFF_ID') string(defaultValue: 'PHID-PROJ-gbo56hpf2y5bi7t5jusk', description: 'ID of the project', name: 'PROJECT_ID') } environment { PHABRICATOR_HOST = 'https://c4science.ch/api/' PYTHONPATH = sh returnStdout: true, script: 'echo ${WORKSPACE}/tests/ci/script/' } agent { dockerfile { additionalBuildArgs '--tag tamaas-environment'} } stages { stage('SCM Checkout') { steps { checkout scm: [ $class: 'GitSCM', branches: scm.branches, extensions: [[ $class: 'SubmoduleOption', recursiveSubmodules: true, ]], userRemoteConfigs: scm.userRemoteConfigs ] } } stage('Configure') { steps { sh '''#!/usr/bin/env bash echo "py_exec = \'python3\'" > build-setup.conf echo "build_python = \'true\'" >> build-setup.conf echo "build_tests = \'true\'" >> build-setup.conf echo "use_googletest = \'true\'" >> build-setup.conf - echo "use_mpi = \'true\'" >> build-setup.conf''' + echo "use_mpi = \'true\'" >> build-setup.conf + echo "verbose = \'true\'" >> build-setup.conf''' } } stage('Compile') { steps { sh '''#!/usr/bin/env bash set -o pipefail rm -rf .sconf_temp .sconsign.dblite build-release scons | tee compilation.txt''' } post { failure { uploadArtifact('compilation.txt', 'Compilation') } } } stage('Run tests') { steps { sh 'PYTHONPATH=$PWD/build-release/python/ python3 -m pytest --durations=0 --junitxml=results.xml build-release/tests' } } } post { always { createArtifact("results.xml") junit 'results.xml' step([$class: 'XUnitBuilder', thresholds: [ [$class: 'SkippedThreshold', failureThreshold: '0'], [$class: 'FailedThreshold', failureThreshold: '0']], tools: [[$class: 'GoogleTestType', pattern: 'build-release/tests/gtest_results.xml']]]) } success { trigger_rtd() passed_hbm() } failure { failed_hbm() emailext( body: '''${SCRIPT, template="groovy-html.template"}''', mimeType: 'text/html', subject: "[Jenkins] ${currentBuild.fullDisplayName} Failed", recipientProviders: [[$class: 'CulpritsRecipientProvider']], to: 'lucas.frerot@protonmail.com', attachLog: true, compressLog: true) } } } def createArtifact(filename) { sh "./tests/ci/scripts/hbm send-uri -k 'Jenkins URI' -u ${BUILD_URL} -l 'View Jenkins result'" sh "./tests/ci/scripts/hbm send-junit-results -f ${filename}" } def uploadArtifact(artifact, name) { sh "./test/ci/scripts/hbm upload-file -f ${artifact} -n \"${name}\" -v ${PROJECT_ID}" } def failed_hbm() { sh "./tests/ci/scripts/hbm failed" } def passed_hbm() { sh "./tests/ci/scripts/hbm passed" } def trigger_rtd() { sh """ set -x curl -X POST -d "token=${RTD_TOKEN}" https://readthedocs.org/api/v2/webhook/tamaas/106141/ """ } diff --git a/SConstruct b/SConstruct index 4c109182..b83e4ff4 100644 --- a/SConstruct +++ b/SConstruct @@ -1,475 +1,478 @@ # -*- mode:python; coding: utf-8 -*- # vim: set ft=python: # @file # LICENSE # # Copyright (©) 2016-2021 EPFL (École Polytechnique Fédérale de Lausanne), # Laboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. # ------------------------------------------------------------------------------ # Imports # ------------------------------------------------------------------------------ from __future__ import print_function import sys import os from subprocess import check_output # Import below not strictly necessary, but good for pep8 from SCons.Script import ( EnsurePythonVersion, EnsureSConsVersion, Help, Environment, Variables, EnumVariable, PathVariable, BoolVariable, ListVariable, Split, Export, Dir, ) from SCons.Errors import StopError from SCons import __version__ as scons_version from version import get_git_subst from detect import ( FindFFTW, FindBoost, FindThrust, FindCuda, FindExpolit, FindPybind11 ) from INFOS import TAMAAS_INFOS # ------------------------------------------------------------------------------ EnsurePythonVersion(2, 7) EnsureSConsVersion(2, 4) # ------------------------------------------------------------------------------ def detect_dependencies(env): "Detect all dependencies" fftw_comp = { 'omp': ['omp'], 'threads': ['threads'], 'none': [], } fftw_components = fftw_comp[env['fftw_threads']] if main_env['use_mpi']: fftw_components.append('mpi') FindFFTW(env, fftw_components, precision=env['real_type']) FindBoost(env, ['boost/preprocessor/seq.hpp']) FindExpolit(env) thrust_var = 'THRUST_ROOT' # Take cuda version of thrust if available if 'CUDA_ROOT' in env['ENV']: thrust_var = 'CUDA_ROOT' FindThrust(env, env['backend'], thrust_var) # Activate cuda if needed if env['backend'] == 'cuda': FindCuda(env) if env['build_python']: FindPybind11(env) def subdir(env, dir): "Building a sub-directory" return env.SConscript(env.File('SConscript', dir), variant_dir=env.Dir(dir, env['build_dir']), duplicate=True) # ------------------------------------------------------------------------------ # Main compilation # ------------------------------------------------------------------------------ # Compilation colors colors = { 'cyan': '\033[96m', 'purple': '\033[95m', 'blue': '\033[94m', 'green': '\033[92m', 'yellow': '\033[93m', 'gray': '\033[38;5;8m', 'orange': '\033[38;5;208m', 'red': '\033[91m', 'end': '\033[0m' } # Inherit all environment variables (for CXX detection, etc.) main_env = Environment( ENV=os.environ, ) # Set tamaas information for k, v in TAMAAS_INFOS.items(): main_env[k] = v main_env['COLOR_DICT'] = colors main_env.AddMethod(subdir, 'SubDirectory') # Build variables vars = Variables('build-setup.conf') vars.AddVariables( EnumVariable('build_type', 'Build type', 'release', allowed_values=('release', 'profiling', 'debug'), ignorecase=2), EnumVariable('backend', 'Thrust backend', 'omp', allowed_values=('cpp', 'omp', 'tbb'), ignorecase=2), EnumVariable('fftw_threads', 'Threads FFTW library preference', 'omp', allowed_values=('omp', 'threads', 'none'), ignorecase=2), EnumVariable('sanitizer', 'Sanitizer type', 'none', allowed_values=('none', 'memory', 'leaks', 'address'), ignorecase=2), PathVariable('prefix', 'Prefix where to install', '/usr/local'), # Dependencies paths PathVariable('FFTW_ROOT', 'FFTW custom path', os.getenv('FFTW_ROOT', ''), PathVariable.PathAccept), PathVariable('THRUST_ROOT', 'Thrust custom path', os.getenv('THRUST_ROOT', ''), PathVariable.PathAccept), PathVariable('BOOST_ROOT', 'Boost custom path', os.getenv('BOOST_ROOT', ''), PathVariable.PathAccept), PathVariable('CUDA_ROOT', 'Cuda custom path', os.getenv('CUDA_ROOT', ''), PathVariable.PathAccept), # Dependencies provided as submodule get different default PathVariable('GTEST_ROOT', 'Googletest custom path', os.getenv('GTEST_ROOT', '#third-party/googletest/googletest'), PathVariable.PathAccept), PathVariable('PYBIND11_ROOT', 'Pybind11 custom path', os.getenv('PYBIND11_ROOT', '#third-party/pybind11/include'), PathVariable.PathAccept), PathVariable('EXPOLIT_ROOT', 'Expolit custom path', os.getenv('EXPOLIT_ROOT', '#third-party/expolit/include'), PathVariable.PathAccept), # Executables ('CXX', 'Compiler', os.getenv('CXX', 'g++')), ('MPICXX', 'MPI Compiler wrapper', os.getenv('MPICXX', 'mpicxx')), ('py_exec', 'Python executable', 'python3'), # Compiler flags ('CXXFLAGS', 'C++ compiler flags', os.getenv('CXXFLAGS', "")), # Cosmetic BoolVariable('verbose', 'Activate verbosity', False), BoolVariable('color', 'Color the non-verbose compilation output', False), # Tamaas components BoolVariable('build_doc', 'Build documentation', False), BoolVariable('build_tests', 'Build test suite', False), BoolVariable('build_python', 'Build python wrapper', True), # Documentation ListVariable('doc_builders', 'Generated documentation formats', default='html', names=Split("html man")), # TODO include latex # Dependencies BoolVariable('use_googletest', 'Build tests using GTest', False), BoolVariable('use_mpi', 'Builds multi-process parallelism', False), # Distribution options BoolVariable('strip_info', 'Strip binary of added information', False), BoolVariable('build_static_lib', "Build a static libTamaas", False), # Type variables EnumVariable('real_type', 'Type for real precision variables', 'double', allowed_values=('double', 'long double')), EnumVariable('integer_type', 'Type for integer variables', 'int', allowed_values=('int', 'long')), ) # Set variables of environment vars.Update(main_env) help_text = vars.GenerateHelpText(main_env) help_text += """ Commands: scons [build] [options]... Compile Tamaas (and additional modules/tests) scons install [prefix=/your/prefix] [options]... Install Tamaas to prefix scons dev Install symlink to Tamaas python module (useful to development purposes) scons test Run tests with pytest scons doc Compile documentation with Doxygen and Sphinx+Breathe scons archive Create a gzipped archive from source """ # noqa Help(help_text) # Save all options, not just those that differ from default with open('build-setup.conf', 'w') as setup: for option in vars.options: setup.write("# " + option.help.replace('\n', '\n# ') + "\n") setup.write("{} = '{}'\n".format(option.key, main_env[option.key])) main_env['should_configure'] = \ not main_env.GetOption('clean') and not main_env.GetOption('help') build_type = main_env['build_type'] build_dir = 'build-${build_type}' main_env['build_dir'] = main_env.Dir(build_dir) # Setting up the python name with version if main_env['build_python']: args = (main_env.subst("${py_exec} -c").split() + ["from distutils.sysconfig import get_python_version;" "print(get_python_version())"]) main_env['py_version'] = bytes(check_output(args)).decode() # Printing some build infos if main_env['should_configure']: print('-- SCons {} (Python {}.{})'.format(scons_version, sys.version_info.major, sys.version_info.minor)) print(main_env.subst("-- Build type: ${build_type}\n" "-- Thrust backend: ${backend}\n" "-- FFTW threads: ${fftw_threads}\n" "-- MPI: ${use_mpi}\n" "-- Build directory: ${build_dir}\n" "-- Python version (bindings): $py_version")) verbose = main_env['verbose'] # Remove colors if not set if not main_env['color']: for key in colors: colors[key] = '' if not verbose: main_env['CXXCOMSTR'] = main_env['SHCXXCOMSTR'] = \ u'{0}[Compiling ($SHCXX)] {1}$SOURCE'.format(colors['green'], colors['end']) main_env['LINKCOMSTR'] = main_env['SHLINKCOMSTR'] = \ u'{0}[Linking] {1}$TARGET'.format(colors['purple'], colors['end']) main_env['ARCOMSTR'] = u'{}[Ar]{} $TARGET'.format(colors['purple'], colors['end']) main_env['RANLIBCOMSTR'] = \ u'{}[Randlib]{} $TARGET'.format(colors['purple'], colors['end']) main_env['PRINT_CMD_LINE_FUNC'] = pretty_cmd_print + main_env['INSTALLSTR'] = \ + u'{}[Installing] {}$SOURCE to $TARGET'.format(colors['blue'], + colors['end']) # Include paths main_env.AppendUnique(CPPPATH=['#/src', '#/src/core', '#/src/mpi', '#/src/bem', '#/src/surface', '#/src/python', '#/src/percolation', '#/src/model', '#/src/model/elasto_plastic', '#/src/solvers', '#/src/gpu', '#/python']) # Changing the shared object extension main_env['SHOBJSUFFIX'] = '.o' # Back to gcc if cuda is activated if main_env['backend'] == "cuda" and "g++" not in main_env['CXX']: raise StopError('GCC should be used when compiling with CUDA') # OpenMP flags - compiler dependent omp_flags = { "g++": ["-fopenmp"], "clang++": ["-fopenmp"], "icpc": ["-qopenmp"] } def cxx_alias(cxx): for k in omp_flags.keys(): if k in cxx: return k raise StopError('Unsupported compiler: ' + cxx) cxx = cxx_alias(main_env['CXX']) # Setting main compilation flags main_env['CXXFLAGS'] = Split(main_env['CXXFLAGS']) main_env['LINKFLAGS'] = main_env['CXXFLAGS'] main_env.AppendUnique( CXXFLAGS=Split('-std=c++14 -Wall -Wextra -pedantic'), CPPDEFINES={ 'TAMAAS_LOOP_BACKEND': 'TAMAAS_LOOP_BACKEND_${backend.upper()}', 'TAMAAS_FFTW_BACKEND': 'TAMAAS_FFTW_BACKEND_${fftw_threads.upper()}' }, ) # Adding OpenMP flags if main_env['backend'] == 'omp': main_env.AppendUnique(CXXFLAGS=omp_flags[cxx]) main_env.AppendUnique(LINKFLAGS=omp_flags[cxx]) else: main_env.AppendUnique(CXXFLAGS=['-Wno-unknown-pragmas']) # Correct bug in clang? if main_env['backend'] == 'omp' and cxx == "clang++": main_env.AppendUnique(LIBS=["atomic"]) elif main_env['backend'] == 'tbb': main_env.AppendUnique(LIBS=['tbb']) # Manage MPI compiler if main_env['use_mpi']: main_env['CXX'] = '$MPICXX' main_env.AppendUnique(CPPDEFINES=['TAMAAS_USE_MPI']) main_env.AppendUnique(CXXFLAGS=['-Wno-cast-function-type']) # Flags and options if main_env['build_type'] == 'debug': main_env.AppendUnique(CPPDEFINES=['TAMAAS_DEBUG']) # Define the scalar types main_env.AppendUnique(CPPDEFINES={'TAMAAS_REAL_TYPE': '${real_type}', 'TAMAAS_INT_TYPE': '${integer_type}'}) # Compilation flags cxxflags_dict = { "debug": Split("-g -O0"), "profiling": Split("-g -O3 -fno-omit-frame-pointer"), "release": Split("-O3") } if main_env['sanitizer'] != 'none': if main_env['backend'] == 'cuda': raise StopError( "Sanitizers with cuda are not yet supported!") cxxflags_dict[build_type].append('-fsanitize=${sanitizer}') main_env.AppendUnique(CXXFLAGS=cxxflags_dict[build_type]) main_env.AppendUnique(SHLINKFLAGS=cxxflags_dict[build_type]) main_env.AppendUnique(LINKFLAGS=cxxflags_dict[build_type]) if main_env['should_configure']: basic_checks(main_env) detect_dependencies(main_env) # Writing information file main_env.Tool('textfile') main_env['SUBST_DICT'] = get_git_subst() # Empty values if requested if main_env['strip_info']: for k in main_env['SUBST_DICT']: main_env['SUBST_DICT'][k] = "" # Substitution of environment file main_env['SUBST_DICT'].update({ '@build_type@': '$build_type', '@build_dir@': '${build_dir.abspath}', '@build_version@': '$version', '@backend@': '$backend', }) # Environment file content env_content = """export PYTHONPATH=@build_dir@/python:$$PYTHONPATH export LD_LIBRARY_PATH=@build_dir@/src:$$LD_LIBRARY_PATH """ # Writing environment file env_file = main_env.Textfile( main_env.File('tamaas_environment.sh', main_env['build_dir']), env_content) # Default targets build_targets = ['build-cpp', env_file] install_targets = ['install-lib'] if main_env._get_major_minor_revision(scons_version)[0] >= 4: main_env.Tool('compilation_db') main_env.CompilationDatabase(PRINT_CMD_LINE_FUNC=pretty_cmd_print) # Building Tamaas library Export('main_env') main_env.SubDirectory('src') # Building Tamaas extra components for dir in ['python', 'tests']: if main_env['build_{}'.format(dir)] and not main_env.GetOption('help'): main_env.SubDirectory(dir) build_targets.append('build-{}'.format(dir)) # Building API + Sphinx documentation if requested if main_env['build_doc']: main_env.SubDirectory('doc') main_env.Alias('doc', 'build-doc') install_targets.append('install-doc') else: dummy_command(main_env, 'doc', 'Command "doc" does not do anything' ' without documentation activated ("build_doc=True")') # Define dummy dev command when python is deactivated if not main_env['build_python']: dummy_command(main_env, 'dev', 'Command "dev" does not do anything' + ' without python activated ("build_python=True")') else: install_targets.append('install-python') # Define dummy test command when tests are deactivated if not main_env['build_tests']: dummy_command(main_env, 'test', 'Command "test" does not do anything' + ' without tests activated ("build_tests=True")') # Definition of target aliases, a.k.a. sub-commands main_env.Alias('build', build_targets) # Define proper install targets main_env.Alias('install', install_targets) # Default target is to build stuff main_env.Default('build') # Building a tar archive archive = main_env.Command( 'tamaas-${version}.tar.gz', '', ('tar --exclude-vcs --exclude-vcs-ignores ' '--exclude=third-party/googletest ' '--exclude=third-party/pybind11 ' '--exclude=joss ' '--exclude=".*" ' '-czf $TARGET {}'.format(Dir('.').name)), chdir='..', ) main_env.Alias('archive', archive) diff --git a/python/SConscript b/python/SConscript index d31d2948..6d0da3bd 100644 --- a/python/SConscript +++ b/python/SConscript @@ -1,160 +1,163 @@ # -*- mode:python; coding: utf-8 -*- # vim: set ft=python: # @file # LICENSE # # Copyright (©) 2016-2021 EPFL (École Polytechnique Fédérale de Lausanne), # Laboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. from __future__ import print_function from SCons.Script import Import, Split, Copy, Dir Import('main_env') # Pybind11 wrapper env_pybind = main_env.Clone(SHLIBPREFIX='') # Remove pedantic warnings cxx_flags = env_pybind['CXXFLAGS'] try: del cxx_flags[cxx_flags.index('-pedantic')] except ValueError: pass env_pybind.Tool(pybind11) pybind_sources = Split(""" tamaas_module.cpp wrap/core.cpp wrap/percolation.cpp wrap/surface.cpp wrap/model.cpp wrap/solvers.cpp wrap/compute.cpp wrap/mpi.cpp wrap/test_features.cpp """) # Setting paths to find libTamaas env_pybind.AppendUnique(LIBPATH=['../src']) # Link against a static libTamaas if env_pybind['build_static_lib']: env_pybind.PrependUnique(LIBS=['Tamaas']) # keep other libs for link env_pybind['RPATH'] = "" # no need for rpath w/ static lib # Link against a dynamic libTamaas else: env_pybind.AppendUnique(RPATH=[ "'$$$$ORIGIN/../../src'", # path to lib in build_dir "'$$$$ORIGIN/../../..'", # path to lib in install prefix ]) env_pybind['LIBS'] = ['Tamaas'] # discard other libs for link # Building the pybind library tamaas_wrap = env_pybind.Pybind11Module( target='tamaas/_tamaas', source=pybind_sources, ) # For some reason link happens too early Import('libTamaas') env_pybind.Depends(tamaas_wrap, libTamaas) # Copying the __init__.py file with extra python classes copy_env = env_pybind.Clone() # Copying additional python files python_files = """ __main__.py compute.py dumpers/__init__.py dumpers/_helper.py nonlinear_solvers/__init__.py """.split() targets = [tamaas_wrap] targets += [ copy_env.Command(copy_env.File(f, 'tamaas'), copy_env.File(f, '#python/tamaas'), Copy("$TARGET", "$SOURCE")) for f in python_files ] targets.append(copy_env.Command('MANIFEST.in', '#python/MANIFEST.in', Copy("$TARGET", "$SOURCE"))) subst_env = env_pybind.Clone( SUBST_DICT={ '@version@': '$version', '@authors@': str(copy_env['authors']), '@email@': '$email', # TODO change when issue with unicode fixed # '@copyright@': '$copyright', # '@maintainer@': '$maintainer', } ) subst_env.Tool('textfile') targets.append(subst_env.Substfile('setup.py.in')) targets.append(subst_env.Substfile('tamaas/__init__.py.in')) # Defining alias for python builds main_env.Alias('build-python', targets) # Checking if we can use pip to install (more convenient for end-user) install_env = main_env.Clone() conf = Configure(install_env, custom_tests={'CheckPythonModule': CheckPythonModule}) has_pip = conf.CheckPythonModule('pip') install_env = conf.Finish() # Current build directory install_env['PYDIR'] = Dir('.') # Setting command line for installation if has_pip: install_env['PYINSTALLCOM'] = '${py_exec} -m pip install -U $PYOPTIONS .' install_env['PYDEVELOPCOM'] = \ '${py_exec} -m pip install $PYOPTIONS -e .[all]' else: install_env['PYINSTALLCOM'] = '${py_exec} setup.py install $PYOPTIONS' install_env['PYDEVELOPCOM'] = '${py_exec} setup.py develop $PYOPTIONS' install_env['py_version'] = get_python_version(install_env) install_env.PrependENVPath( 'PYTHONPATH', install_env.subst('${prefix}/lib/python${py_version}/site-packages')) # Specify install target +PYOPTIONS = ['${"" if verbose else "-q"}'] python_install = install_env.Command( '.python_install_phony', - targets, install_env['PYINSTALLCOM'], PYOPTIONS='--prefix ${prefix}', + targets, install_env['PYINSTALLCOM'], + PYOPTIONS=['--prefix', '${prefix}'] + PYOPTIONS, chdir=install_env['PYDIR']) python_install_dev = install_env.Command( '.python_install_local_phony', - targets, install_env['PYDEVELOPCOM'], PYOPTIONS='--user', + targets, install_env['PYDEVELOPCOM'], + PYOPTIONS=['--user'] + PYOPTIONS, chdir=install_env['PYDIR']) # Defining aliases main_env.Alias('install-python', python_install) main_env.Alias('dev', python_install_dev) diff --git a/site_scons/site_init.py b/site_scons/site_init.py index 39205752..99b39e1c 100644 --- a/site_scons/site_init.py +++ b/site_scons/site_init.py @@ -1,179 +1,191 @@ # -*- coding: utf-8 -*- # @file # LICENSE # # Copyright (©) 2016-2021 EPFL (École Polytechnique Fédérale de Lausanne), # Laboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. import subprocess from detect import FindPybind11 from SCons.Script import Configure from SCons.Errors import StopError # ------------------------------------------------------------------------------ def pybind11(env): """A tool to configure pybind11""" def execute(command): return [line for line in subprocess.check_output( command, universal_newlines=True).split('\n') if line != ""] # Create a clone so we do not modify env clone = env.Clone() # Set variables for clone FindPybind11(clone) includes = clone['CPPPATH'] # Extension of shared library for python try: extension = execute( env.subst('${py_exec}-config --extension-suffix').split())[0] except subprocess.CalledProcessError: extension = ".so" def pybind11_builder(env, target, source, **kwargs): """Create a pybind11 module""" clone = env.Clone() clone.AppendUnique(CPPPATH=includes) clone['SHLIBSUFFIX'] = extension clone.AppendUnique(SHLINKFLAGS=['-fvisibility=hidden']) return clone.SharedLibrary(target, source, **kwargs) # Add pseudo-builder to master environment env.AddMethod(pybind11_builder, 'Pybind11Module') # ------------------------------------------------------------------------------ def pretty_cmd_print(command, target, source, env): colors = env['COLOR_DICT'] if 'Copy' in command: color = colors['gray'] action = 'Copying' elif 'Creating' in command: color = colors['yellow'] action = 'Generating' elif 'database' in command: color = colors['yellow'] action = 'Generating' + elif 'pytest' in command: + color = colors['green'] + action = 'Running tests' + target[0] = target[0].dir + elif 'pip' in command: + color = colors['blue'] + if 'user' in command: + action = 'Symlinking' + target[0] = target[0].dir + elif 'prefix' in command: + action = 'Installing' + target[0] = env.subst(target[0].dir.path + ' to ${prefix}') else: print(command) return print("{color}[{action}] {end}{target}".format( color=color, end=colors['end'], target=target[0], action=action )) # ------------------------------------------------------------------------------ def CheckPythonModule(context, module): """Checks the existence of a python module""" context.Message('Checking for Python module {}... '.format(module)) env = context.sconf.env command = [env.subst('${py_exec}'), '-c', 'import {}'.format(module)] context.Log('Executing external command: {}\n'.format(command)) try: subprocess.check_output(command, stderr=subprocess.STDOUT) result = True except subprocess.CalledProcessError as e: result = False output = bytes(e.output) context.Log(output.decode() + '\n') context.Result(result) return result # ------------------------------------------------------------------------------ def CheckCompilerFlag(context, flag): "Check compiler provides flag" context.Message('Checking compiler flag {}... '.format( context.sconf.env.subst(flag))) test_file = """ int main() { return 0; } """ env = context.sconf.env.Clone(CXXFLAGS=[flag], LINKFLAGS=[flag]) context.sconf.env = env result = context.TryLink(test_file, '.cpp') context.Result(result) return result # ------------------------------------------------------------------------------ def dummy_command(env, command, error_msg): """Creates a dummy scons command""" def print_error(*args, **kwargs): print(error_msg) def print_cmd(*args, **kwargs): pass comm = env.Command('#.phony_{}'.format(command), '', print_error, PRINT_CMD_LINE_FUNC=print_cmd) env.Alias(command, comm) # ------------------------------------------------------------------------------ def get_python_version(env): versions_script = """ from __future__ import print_function from sysconfig import get_python_version print(get_python_version())""" version = subprocess.check_output([env['py_exec'], "-c", versions_script], universal_newlines=True).replace('\n', '') print(version) return version # ------------------------------------------------------------------------------ def basic_checks(env): custom_tests = { 'CheckCompilerFlag': CheckCompilerFlag, } conf = Configure(env, custom_tests=custom_tests) if not conf.CheckCXX(): StopError('Could not find working C++ compiler') for flag in env['CXXFLAGS']: if not conf.CheckCompilerFlag(flag): StopError('Compiler does not support flag {}'.format(flag)) def check_type(cpp_type, stl_header): if not conf.CheckType(cpp_type, '#include <{}>'.format(stl_header), 'cpp'): StopError('Standard type {} is not available'.format(cpp_type)) check_type('std::multiplies<void>', 'functional') check_type('std::unique_ptr<void>', 'memory') conf.Finish() diff --git a/tests/SConscript b/tests/SConscript index eeba2985..737f1efa 100644 --- a/tests/SConscript +++ b/tests/SConscript @@ -1,213 +1,215 @@ # -*- mode:python; coding: utf-8 -*- # vim: set ft=python: # @file # LICENSE # # Copyright (©) 2016-2021 EPFL (École Polytechnique Fédérale de Lausanne), # Laboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. from __future__ import print_function from SCons.Script import Split, Copy, Dir, Import from detect import FindGTest, FindPybind11 # ------------------------------------------------------------------------------ def copyComStr(env, main): if 'SHCXXCOMSTR' in main: env['CXXCOMSTR'] = main['SHCXXCOMSTR'] if 'SHLINKCOMSTR' in main: env['LINKCOMSTR'] = main['SHLINKCOMSTR'] # ------------------------------------------------------------------------------ def make_python_tests(env): """Copy python tests to build directory""" test_env = env.Clone() test_files = Split(""" test_hertz.py test_westergaard.py test_patch_westergaard.py test_patch_plasticity.py test_surface.py test_hertz_disp.py test_hertz_kato.py test_saturated_pressure.py test_flood_fill.py test_integral_operators.py test_dumper.py test_tangential.py test_boussinesq_surface.py test_voigt.py test_memory.py test_epic.py fftfreq.py conftest.py pytest.ini """) if env['use_mpi']: test_files += ['test_mpi_routines.py', 'mpi_routines.py'] src_dir = "#/tests" targets = [ test_env.Command(file, test_env.File(file, src_dir), Copy("$TARGET", "$SOURCE")) for file in test_files ] test_env = env.Clone(tools=[pybind11]) # Helper module for integral operators test_env['SHLIBPREFIX'] = '' test_env.PrependUnique(LIBS=['Tamaas']) register = test_env.Pybind11Module( target="register_integral_operators", source=["register_integral_operators.cpp"]) Import('libTamaas') test_env.Depends(register, libTamaas) targets.append(register) return targets # ------------------------------------------------------------------------------ def compile_google_test(env, gtest_path): gtest_obj = env.Object('gtest.o', [env.File("src/gtest-all.cc", gtest_path)]) return env.StaticLibrary('gtest', gtest_obj) # ------------------------------------------------------------------------------ def make_google_tests(env): gtest_dir = Dir(env['GTEST_ROOT']) gtest_env = env.Clone(CPPPATH=[gtest_dir], CXXFLAGS=['-pthread', '-isystem', env.Dir('include', gtest_dir).path]) FindGTest(gtest_env) libgtest = None # Hugly hack to detect if we need to compile gtest submodule if env['GTEST_ROOT'] == '#third-party/googletest/googletest': gtest_path = str(gtest_dir) libgtest = compile_google_test(gtest_env, gtest_path) env.AppendUnique(CXXFLAGS=gtest_env['CXXFLAGS']) env.PrependUnique(LIBS=['Tamaas', env.subst('python${py_version}')]) google_test_files = Split(""" test_fft.cpp test_grid.cpp test_loop.cpp test_model.cpp test_static_types.cpp test_integration.cpp """) # Necessary for the tests that use pybind11 calls to python uses = [] if env['build_python']: google_test_files.append('test_fftfreq.cpp') uses = ['TAMAAS_USE_PYTHON'] if env['use_mpi']: google_test_files.append('test_mpi.cpp') defines = env['CPPDEFINES'] if type(defines) is not list: defines = [defines] gtest_main = env.Object("tamaas_gtest_main.o", 'tamaas_gtest_main.cc', CPPDEFINES=defines + uses) gtest_all = env.Program('test_gtest_all', google_test_files + [gtest_main], LIBS=(env['LIBS'] + ['gtest'])) Import('libTamaas') env.Depends(gtest_all, libTamaas) env.Depends(gtest_all, libgtest) return [gtest_all] # ------------------------------------------------------------------------------ def make_bare_tests(env): rough = env.Program("test_rough_surface.cpp") Import('libTamaas') env.Depends(rough, libTamaas) return [rough] # ------------------------------------------------------------------------------ Import('main_env') # Setup of test environment test_env = main_env.Clone() test_env.AppendUnique( LIBPATH=['.', '../src'], RPATH=["'$$$$ORIGIN/../src'"] ) test_env.PrependUnique(LIBS=['Tamaas']) # Building tests that do not require any third party targets = make_bare_tests(test_env) # Build tests that required python bindings if test_env['build_python']: FindPybind11(test_env) test_env.Tool(pybind11) test_env.ParseConfig("${py_exec}-config --ldflags") test_env['CCFLAGS'] = [] targets += make_python_tests(test_env) # Building google tests if test_env['use_googletest']: targets += make_google_tests(test_env) targets.append(test_env.Command('test_gtest.py', '#tests/test_gtest.py', Copy('$TARGET', '$SOURCE'))) # Target alias to build tests main_env.Alias('build-tests', targets) # Check if pytest is installed conf = Configure(test_env, custom_tests={'CheckPythonModule': CheckPythonModule}) has_pytest = conf.CheckPythonModule('pytest') conf.Finish() # Define a command to execute tests if has_pytest: pytest_env = test_env.Clone() test_env['pythonpath'] = '${build_dir}/python' test_env['ld_library_path'] = '${build_dir}/src' pytest_env.PrependENVPath('PYTHONPATH', '${pythonpath.abspath}') pytest_env.PrependENVPath('LD_LIBRARY_PATH', '${ld_library_path.abspath}') # Setting a moderate thread number pytest_env['ENV']['OMP_NUM_THREADS'] = "1" - test_target = pytest_env.Command('.phony_test', targets, - '${py_exec} -m pytest ${build_dir}/tests') + pytest_env['PYTESTOPTS'] = ['${"-v" if verbose else "-q"}'] + test_target = pytest_env.Command( + '.phony_test', targets, + '${py_exec} -m pytest $PYTESTOPTS ${TARGET.dir}') main_env.Alias('test', test_target) else: # We still define a target here so that `scons test` still works dummy_command(main_env, 'test', 'Cannot run tests: pytest is not installed') diff --git a/tests/conftest.py b/tests/conftest.py index f1fb9f78..1c9337d8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,309 +1,302 @@ # -*- coding: utf-8 -*- # @file # LICENSE # # Copyright (©) 2016-2021 EPFL (École Polytechnique Fédérale de Lausanne), # Laboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. from __future__ import division, print_function import subprocess import sys from functools import reduce from operator import mul as multiply import numpy as np import pytest from numpy.linalg import norm try: import tamaas as tm except ModuleNotFoundError as e: print(e, file=sys.stderr) print("Use 'scons test' to run tests", file=sys.stderr) sys.exit(1) type_list = [tm.model_type.basic_1d, tm.model_type.basic_2d, tm.model_type.surface_1d, tm.model_type.surface_2d, tm.model_type.volume_1d, tm.model_type.volume_2d] def get_libtamaas_file(): try: ldd_out = bytes(subprocess.check_output(['ldd', tm._tamaas.__file__])) for line in filter(lambda x: 'Tamaas' in x, ldd_out.decode().split('\n')): path = line.split(' => ')[1] return path.split(' ')[0] return "static link" except subprocess.CalledProcessError: return "N/A" def pytest_report_header(config): print('tamaas build type: {}\nmodule file: {}\nwrapper: {}\nlibTamaas: {}' .format(tm.TamaasInfo.build_type, tm.__file__, tm._tamaas.__file__, get_libtamaas_file())) def pytest_addoption(parser): pass # try: # parser.addoption( # '--with-mpi', action="store_true", default=False, # help="Run MPI tests, this should be paired with mpirun." # ) # except ValueError: # pass def mtidfn(val): return str(val).split('.')[-1] def pkridfn(val): return {tm.PolonskyKeerRey.pressure: "pkr_pressure", tm.PolonskyKeerRey.gap: "pkr_gap"}[val] def profile(func, domain, N, modes, amplitude): coords = [np.linspace(0, d, n, endpoint=False, dtype=tm.dtype) for n, d in zip(N, domain)] coords = np.meshgrid(*coords, indexing='ij') sines = [func(2 * np.pi * x * m) for x, m in zip(coords, modes)] return reduce(multiply, [amplitude] + sines) -@pytest.fixture(scope="package", autouse=True) -def tamaas_fixture(): - tm.initialize() - yield None - tm.finalize() - - class HertzFixture: def __init__(self, n, load): self.domain_size = 1 self.n = n self.load = load self.curvature = 0.1 self.radius = 1. / self.curvature self.e_star = 1. self.a = (3 * load / (4 * self.curvature * self.e_star))**(1. / 3.) self.x = np.linspace(-self.domain_size / 2., self.domain_size / 2., self.n, dtype=tm.dtype) self.y = self.x.copy() self.x, self.y = np.meshgrid(self.x, self.y) self._computeSurface() self._computePressure() self._computeDisplacement() def _computeDisplacement(self): r = np.sqrt(self.x**2 + self.y**2) self.displacement = np.zeros_like(r) contact = r < self.a self.displacement[contact] = self.surface[contact] self.displacement[~contact] = \ (self.surface[~contact] + self.a / (np.pi * self.radius) * np.sqrt(r[~contact]**2 - self.a**2) + (r[~contact]**2 - 2 * self.a**2) / (np.pi * self.radius) * np.arccos(self.a / r[~contact])) def _computePressure(self): r = np.sqrt(self.x**2 + self.y**2) self.pressure = np.zeros_like(r) contact = np.where(r < self.a) self.pressure[contact] = \ 2 * self.e_star / (np.pi * self.radius) \ * np.sqrt(self.a**2 - r[contact]**2) def _computeSurface(self): self.surface = -1. / (2 * self.radius) * (self.x**2 + self.y**2) @pytest.fixture(scope="package") def hertz(): return HertzFixture(1024, 0.00001) @pytest.fixture(scope="package") def hertz_coarse(): return HertzFixture(512, 0.0001) @pytest.fixture(scope="package", params=[tm.PolonskyKeerRey.pressure], ids=pkridfn) def pkr(hertz, request): model = tm.ModelFactory.createModel(tm.model_type.basic_2d, [hertz.domain_size, hertz.domain_size], [hertz.n, hertz.n]) model.E, model.nu = hertz.e_star, 0 solver = tm.PolonskyKeerRey(model, hertz.surface, 1e-12, request.param, request.param) solver.solve(hertz.load) return model, hertz class WestergaardFixture: def __init__(self, n, load): self.domain_size = 1. self.lamda = 1. self.delta = 0.1 self.e_star = 1. self.n = n self.p_star = np.pi * self.e_star * self.delta / self.lamda self.load = load * self.p_star self.a = self.lamda / np.pi \ * np.arcsin(np.sqrt(self.load / self.p_star)) self.x = np.linspace(-self.domain_size / 2., self.domain_size / 2., self.n, endpoint=False, dtype=tm.dtype) self._computeSurface() self._computePressure() self._computeDisplacement() def _computeSurface(self): self.surface = self.delta * np.cos(2 * np.pi * self.x / self.lamda) def _computePressure(self): self.pressure = np.zeros_like(self.surface) contact = np.where(np.abs(self.x) < self.a) self.pressure[contact] = 2 * self.load \ * (np.cos(np.pi * self.x[contact] / self.lamda) / np.sin(np.pi * self.a / self.lamda)**2) \ * np.sqrt(np.sin(np.pi * self.a / self.lamda)**2 - np.sin(np.pi * self.x[contact] / self.lamda)**2) def _computeDisplacement(self): psi = np.pi * np.abs(self.x) / self.lamda psi_a = np.pi * self.a / self.lamda with np.errstate(invalid='ignore'): # get some warnings out of the way self.displacement = (np.cos(2*psi) + 2 * np.sin(psi) * np.sqrt(np.sin(psi)**2 - np.sin(psi_a)**2) - 2 * np.sin(psi_a)**2 * np.log((np.sin(psi) + np.sqrt(np.sin(psi)**2 - np.sin(psi_a)**2)) / np.sin(psi_a))) contact = np.where(np.abs(self.x) < self.a) self.displacement[contact] = np.cos(2*psi[contact]) self.displacement *= self.load * self.lamda / (np.pi * self.e_star * np.sin(psi_a)**2) @pytest.fixture(scope="package") def westergaard(): return WestergaardFixture(19683, 0.1) class PatchWestergaard: def __init__(self, model_type): dim = tm.type_traits[model_type].dimension bdim = tm.type_traits[model_type].boundary_dimension domain = [1.] * dim size = [6] * dim self.modes = np.random.randint(1, 3, (bdim,)) self.model = tm.ModelFactory.createModel(model_type, domain, size) self.model.E = 3. self.model.nu = 0. self.pressure = profile(np.cos, self.model.boundary_system_size, self.model.boundary_shape, self.modes, 1) self.solution = profile( np.cos, self.model.boundary_system_size, self.model.boundary_shape, self.modes, 1 / (np.pi * self.model.E_star * norm(self.modes))) @pytest.fixture(scope="package", params=set(type_list) - {tm.model_type.volume_1d}, ids=mtidfn) def patch_westergaard(request): return PatchWestergaard(request.param) class UniformPlasticity: def __init__(self, model_type, domain, sizes): self.n = sizes self.domain = domain self.model = tm.ModelFactory.createModel(model_type, domain, sizes) self.E_h = 0.1 self.sigma_y = 0.01 self.residual = tm.ModelFactory.createResidual(self.model, sigma_y=self.sigma_y, hardening=self.E_h) self.model.E = 1. self.model.nu = 0. def solution(self, p): E, nu = self.model.E, self.model.nu E_h, sigma_y = self.E_h, self.sigma_y mu = E / (2 * (1 + nu)) strain = -1 / (mu + E_h) * (p * (3 * mu + E_h) / (2 * mu) - np.sign(p) * sigma_y) dep = (2 * mu * np.abs(strain) - sigma_y) / (3 * mu + E_h) plastic_strain = np.sign(strain) / 2 * dep * np.array([ -1, -1, 2, 0, 0, 0, ], dtype=tm.dtype) stress = 2 * mu * (np.array([ 0, 0, strain, 0, 0, 0 ], dtype=tm.dtype) - plastic_strain) return { "stress": stress, "plastic_strain": plastic_strain, "cumulated_plastic_strain": dep }, { "stress": mu, "plastic_strain": 1, "cumulated_plastic_strain": 1 } @pytest.fixture(params=[tm.model_type.volume_2d], ids=mtidfn) def patch_isotropic_plasticity(request): return UniformPlasticity(request.param, [1., 1., 1.], [4, 4, 4])