From f8387b50f0d3f2e9391be335e8806d076229faa7 Mon Sep 17 00:00:00 2001 From: Martin Bauer <martin.bauer@fau.de> Date: Sat, 27 Apr 2019 18:39:19 +0200 Subject: [PATCH] Added CI and test files --- .flake8 | 5 + .gitignore | 13 ++ .gitlab-ci.yml | 133 ++++++++++++++++ conftest.py | 117 ++++++++++++++ doc/conf.py | 54 ++++++- doc/img/logo.svg | 248 ++++++++++++++++++++++++++++- doc/img/logo_no_text.png | Bin 0 -> 12172 bytes doc/version_from_git.py | 31 ++++ lbmpy_tests/test_chapman_enskog.py | 27 +--- pytest.ini | 37 +++++ setup.py | 50 +++++- 11 files changed, 675 insertions(+), 40 deletions(-) create mode 100644 .flake8 create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 conftest.py create mode 100644 doc/img/logo_no_text.png create mode 100644 doc/version_from_git.py create mode 100644 pytest.ini diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..a2cf044e --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length=120 +exclude=lbmpy/plot2d.py + lbmpy/session.py +ignore = W293 W503 W291 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..73c1ace7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +__pycache__ +.ipynb_checkpoints +.coverage +*.pyc +*.vti +/build +/dist +/*.egg-info +.cache +_build +/.idea +.cache +_local_tmp \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..9ca03994 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,133 @@ +stages: + - test + - deploy + + +# -------------------------- Tests ------------------------------------------------------------------------------------ + +# Normal test - runs on every commit all but "long run" tests +tests-and-coverage: + stage: test + except: + variables: + - $ENABLE_NIGHTLY_BUILDS + image: i10git.cs.fau.de:5005/software/pystencils/full + script: + - export NUM_CORES=$(nproc --all) + - mkdir -p ~/.config/matplotlib + - echo "backend:template" > ~/.config/matplotlib/matplotlibrc + - mkdir public + - py.test -v -n $NUM_CORES --cov-report html --cov-report term --cov=. -m "not longrun" + tags: + - docker + - cuda + - AVX + artifacts: + when: always + paths: + - coverage_report + +# Nightly test - runs "long run" jobs only +test-longrun: + stage: test + only: + variables: + - $ENABLE_NIGHTLY_BUILDS + image: i10git.cs.fau.de:5005/software/pystencils/full + script: + - export NUM_CORES=$(nproc --all) + - mkdir -p ~/.config/matplotlib + - echo "backend:template" > ~/.config/matplotlib/matplotlibrc + - py.test -v -n $NUM_CORES --cov-report html --cov-report term --cov=. + tags: + - docker + - cuda + - AVX + artifacts: + paths: + - coverage_report + +# Minimal tests in windows environment +minimal-windows: + stage: test + except: + variables: + - $ENABLE_NIGHTLY_BUILDS + tags: + - win + script: + - source /cygdrive/c/Users/build/Miniconda3/Scripts/activate + - source activate pystencils_dev + - env + - conda env list + - python -c "import numpy" + - python setup.py quicktest + +minimal-ubuntu: + stage: test + except: + variables: + - $ENABLE_NIGHTLY_BUILDS + image: i10git.cs.fau.de:5005/software/pystencils/minimal_ubuntu + script: + - python3 setup.py quicktest + tags: + - docker + +minimal-conda: + stage: test + except: + variables: + - $ENABLE_NIGHTLY_BUILDS + image: i10git.cs.fau.de:5005/software/pystencils/minimal_conda + script: + - python setup.py quicktest + tags: + - docker + + +# -------------------- Linter & Documentation -------------------------------------------------------------------------- + + +flake8-lint: + stage: test + except: + variables: + - $ENABLE_NIGHTLY_BUILDS + image: i10git.cs.fau.de:5005/software/pystencils/full + script: + - flake8 lbmpy + tags: + - docker + - cuda + + +build-documentation: + stage: test + image: i10git.cs.fau.de:5005/software/pystencils/full + script: + - export PYTHONPATH=`pwd` + - mkdir html_doc + - sphinx-build -W -b html doc html_doc + tags: + - docker + - cuda + artifacts: + paths: + - html_doc + + +pages: + image: i10git.cs.fau.de:5005/software/pystencils/full + stage: deploy + script: + - ls -l + - mv coverage_report html_doc + - mv html_doc public # folder has to be named "public" for gitlab to publish it + artifacts: + paths: + - public + tags: + - docker + only: + - master@pycodegen/lbmpy diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..6c17a79a --- /dev/null +++ b/conftest.py @@ -0,0 +1,117 @@ +import os +import pytest +import tempfile +import runpy +import sys +# Trigger config file reading / creation once - to avoid race conditions when multiple instances are creating it +# at the same time +from pystencils.cpu import cpujit + +# trigger cython imports - there seems to be a problem when multiple processes try to compile the same cython file +# at the same time +try: + import pyximport + pyximport.install(language_level=3) +except ImportError: + pass + + +SCRIPT_FOLDER = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.abspath('lbmpy')) + + +def add_path_to_ignore(path): + if not os.path.exists(path): + return + global collect_ignore + collect_ignore += [os.path.join(SCRIPT_FOLDER, path, f) for f in os.listdir(os.path.join(SCRIPT_FOLDER, path))] + + +collect_ignore = [os.path.join(SCRIPT_FOLDER, "doc", "conf.py"), + os.path.join(SCRIPT_FOLDER, "doc", "img", "mb_discretization", "maxwell_boltzmann_stencil_plot.py")] +add_path_to_ignore('pystencils_tests/benchmark') +add_path_to_ignore('_local_tmp') + + +try: + import pycuda +except ImportError: + collect_ignore += [os.path.join(SCRIPT_FOLDER, "lbmpy_tests/test_cpu_gpu_equivalence.py")] + +try: + import waLBerla +except ImportError: + collect_ignore += [os.path.join(SCRIPT_FOLDER, "lbmpy_tests/test_serial_scenarios.py")] + collect_ignore += [os.path.join(SCRIPT_FOLDER, "lbmpy_tests/test_datahandling_parallel.py")] + + +collect_ignore += [os.path.join(SCRIPT_FOLDER, 'setup.py')] + +for root, sub_dirs, files in os.walk('.'): + for f in files: + if f.endswith(".ipynb") and not any(f.startswith(k) for k in ['demo', 'tutorial', 'test', 'doc']): + collect_ignore.append(f) + + +import nbformat +from nbconvert import PythonExporter + + +class IPythonMockup: + def run_line_magic(self, *args, **kwargs): + pass + + def run_cell_magic(self, *args, **kwargs): + pass + + def magic(self, *args, **kwargs): + pass + + def __bool__(self): + return False + + +class IPyNbTest(pytest.Item): + def __init__(self, name, parent, code): + super(IPyNbTest, self).__init__(name, parent) + self.code = code + self.add_marker('notebook') + + def runtest(self): + global_dict = {'get_ipython': lambda: IPythonMockup(), + 'is_test_run': True} + + # disable matplotlib output + exec("import matplotlib.pyplot as p; " + "p.switch_backend('Template')", global_dict) + + # in notebooks there is an implicit plt.show() - if this is not called a warning is shown when the next + # plot is created. This warning is suppressed here + exec("import warnings;" + "warnings.filterwarnings('ignore', 'Adding an axes using the same arguments as a previous.*')", + global_dict) + with tempfile.NamedTemporaryFile() as f: + f.write(self.code.encode()) + f.flush() + runpy.run_path(f.name, init_globals=global_dict, run_name=self.name) + + +class IPyNbFile(pytest.File): + def collect(self): + exporter = PythonExporter() + exporter.exclude_markdown = True + exporter.exclude_input_prompt = True + + notebook_contents = self.fspath.open() + notebook = nbformat.read(notebook_contents, 4) + code, _ = exporter.from_notebook_node(notebook) + yield IPyNbTest(self.name, self, code) + + def teardown(self): + pass + + +def pytest_collect_file(path, parent): + glob_exprs = ["*demo*.ipynb", "*tutorial*.ipynb", "test_*.ipynb"] + if any(path.fnmatch(g) for g in glob_exprs): + return IPyNbFile(path, parent) diff --git a/doc/conf.py b/doc/conf.py index a56be9f7..befdb443 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,13 +1,59 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # +import datetime +import sphinx_rtd_theme import os import sys -sys.path.insert(0, os.path.abspath('..')) -sys.path.insert(0, os.path.abspath('../../pystencils')) -sys.path.insert(0, os.path.abspath('../..')) -from sphinx_doc_conf import * +sys.path.insert(0, os.path.abspath('.')) +from version_from_git import version_number_from_git + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.mathjax', + 'sphinx.ext.napoleon', + 'nbsphinx', + 'sphinxcontrib.bibtex', + 'sphinx_autodoc_typehints', +] + +add_module_names = False +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' + +copyright = '{}, Martin Bauer'.format(datetime.datetime.now().year) +author = 'Martin Bauer' +version = version_number_from_git() +release = version_number_from_git() +language = None +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '**.ipynb_checkpoints'] +default_role = 'any' +pygments_style = 'sphinx' +todo_include_todos = False + +# Options for HTML output + +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_theme = 'sphinx_rtd_theme' +htmlhelp_basename = 'pystencilsdoc' +html_sidebars = {'**': ['globaltoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html']} + +# NbSphinx configuration +nbsphinx_execute = 'never' +nbsphinx_codecell_lexer = 'python3' + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'python': ('https://docs.python.org/3.6', None), + 'numpy': ('https://docs.scipy.org/doc/numpy/', None), + 'matplotlib': ('https://matplotlib.org/', None), + 'sympy': ('https://docs.sympy.org/latest/', None), + } + +autodoc_member_order = 'bysource' project = 'lbmpy' html_logo = "img/logo.png" diff --git a/doc/img/logo.svg b/doc/img/logo.svg index ee0a4c64..54e8e0e4 100644 --- a/doc/img/logo.svg +++ b/doc/img/logo.svg @@ -480,6 +480,158 @@ effect="spiro" id="path-effect4188-7" is_visible="true" /> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-3" + style="overflow:visible" + inkscape:isstock="true"> + <path + id="path1421-6" + d="M 0,0 5,-5 -12.5,0 5,5 Z" + style="fill:#dddddd;fill-opacity:1;fill-rule:evenodd;stroke:#dddddd;stroke-width:1.00000003pt;stroke-opacity:1" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" + inkscape:connector-curvature="0" /> + </marker> + <inkscape:path-effect + effect="spiro" + id="path-effect1404-75" + is_visible="true" /> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-8-3" + style="overflow:visible" + inkscape:isstock="true"> + <path + inkscape:connector-curvature="0" + id="path1421-2-5" + d="M 0,0 5,-5 -12.5,0 5,5 Z" + style="fill:#dddddd;fill-opacity:1;fill-rule:evenodd;stroke:#dddddd;stroke-width:1.00000003pt;stroke-opacity:1" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" /> + </marker> + <inkscape:path-effect + effect="spiro" + id="path-effect1404-7-6" + is_visible="true" /> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-8-6-2" + style="overflow:visible" + inkscape:isstock="true"> + <path + inkscape:connector-curvature="0" + id="path1421-2-7-9" + d="M 0,0 5,-5 -12.5,0 5,5 Z" + style="fill:#dddddd;fill-opacity:1;fill-rule:evenodd;stroke:#dddddd;stroke-width:1.00000003pt;stroke-opacity:1" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" /> + </marker> + <inkscape:path-effect + effect="spiro" + id="path-effect1404-7-7-1" + is_visible="true" /> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-8-2-27" + style="overflow:visible" + inkscape:isstock="true"> + <path + inkscape:connector-curvature="0" + id="path1421-2-2-0" + d="M 0,0 5,-5 -12.5,0 5,5 Z" + style="fill:#dddddd;fill-opacity:1;fill-rule:evenodd;stroke:#dddddd;stroke-width:1.00000003pt;stroke-opacity:1" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" /> + </marker> + <inkscape:path-effect + effect="spiro" + id="path-effect1404-7-3-9" + is_visible="true" /> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-8-2-6-36" + style="overflow:visible" + inkscape:isstock="true"> + <path + inkscape:connector-curvature="0" + id="path1421-2-2-5-0" + d="M 0,0 5,-5 -12.5,0 5,5 Z" + style="fill:#dddddd;fill-opacity:1;fill-rule:evenodd;stroke:#dddddd;stroke-width:1.00000003pt;stroke-opacity:1" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" /> + </marker> + <inkscape:path-effect + effect="spiro" + id="path-effect1404-7-3-7-6" + is_visible="true" /> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-8-2-6-3-2" + style="overflow:visible" + inkscape:isstock="true"> + <path + inkscape:connector-curvature="0" + id="path1421-2-2-5-9-61" + d="M 0,0 5,-5 -12.5,0 5,5 Z" + style="fill:#dddddd;fill-opacity:1;fill-rule:evenodd;stroke:#dddddd;stroke-width:1.00000003pt;stroke-opacity:1" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" /> + </marker> + <inkscape:path-effect + effect="spiro" + id="path-effect1404-7-3-7-4-8" + is_visible="true" /> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-8-2-2-7" + style="overflow:visible" + inkscape:isstock="true"> + <path + inkscape:connector-curvature="0" + id="path1421-2-2-8-9" + d="M 0,0 5,-5 -12.5,0 5,5 Z" + style="fill:#dddddd;fill-opacity:1;fill-rule:evenodd;stroke:#dddddd;stroke-width:1.00000003pt;stroke-opacity:1" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" /> + </marker> + <inkscape:path-effect + effect="spiro" + id="path-effect1404-7-3-4-2" + is_visible="true" /> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-8-6-9-0" + style="overflow:visible" + inkscape:isstock="true"> + <path + inkscape:connector-curvature="0" + id="path1421-2-7-4-2" + d="M 0,0 5,-5 -12.5,0 5,5 Z" + style="fill:#dddddd;fill-opacity:1;fill-rule:evenodd;stroke:#dddddd;stroke-width:1.00000003pt;stroke-opacity:1" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" /> + </marker> + <inkscape:path-effect + effect="spiro" + id="path-effect1404-7-7-5-3" + is_visible="true" /> </defs> <sodipodi:namedview id="base" @@ -488,16 +640,16 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="0.98994949" - inkscape:cx="159.4121" - inkscape:cy="-32.506835" + inkscape:zoom="1.4" + inkscape:cx="158.26067" + inkscape:cy="-4.9825309" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" - inkscape:window-width="498" - inkscape:window-height="394" - inkscape:window-x="2210" - inkscape:window-y="646" + inkscape:window-width="1214" + inkscape:window-height="1052" + inkscape:window-x="1482" + inkscape:window-y="524" inkscape:window-maximized="0" fit-margin-top="0" fit-margin-left="0" @@ -622,5 +774,87 @@ d="m 36.797679,33.475 11.90625,13.229166" style="fill:none;fill-rule:evenodd;stroke:#dddddd;stroke-width:0.84519458;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send-8-2-2)" /> </g> + <rect + style="opacity:1;fill:#646ecb;fill-opacity:1;stroke:#d2d2d2;stroke-width:0.5091567;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect1396-7" + width="53.404633" + height="53.404633" + x="9.9747782" + y="82.509102" + ry="3.0735996" + inkscape:export-xdpi="188.45" + inkscape:export-ydpi="188.45" /> + <path + style="fill:none;fill-rule:evenodd;stroke:#dddddd;stroke-width:1.22452438;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send-8-6-9-0)" + d="M 36.299116,109.56879 H 17.132601" + id="path1402-8-0-4-2" + inkscape:connector-curvature="0" + inkscape:path-effect="#path-effect1404-7-7-5-3" + inkscape:original-d="m 36.299116,109.56879 c -6.171351,0.0481 -12.995819,-0.0487 -19.166515,0" + sodipodi:nodetypes="cc" + inkscape:export-xdpi="188.45" + inkscape:export-ydpi="188.45" /> + <g + id="g9842-8" + inkscape:export-xdpi="188.45" + inkscape:export-ydpi="188.45" + transform="matrix(1.4488076,0,0,1.4488076,-17.013641,61.069964)"> + <path + sodipodi:nodetypes="cc" + inkscape:original-d="m 36.797679,33.475 c 2.23e-4,-4.259735 2.23e-4,-8.969879 0,-13.229167" + inkscape:path-effect="#path-effect1404-75" + inkscape:connector-curvature="0" + id="path1402-9" + d="M 36.797679,33.475 V 20.245833" + style="fill:none;fill-rule:evenodd;stroke:#dddddd;stroke-width:0.84519458;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send-3)" /> + <path + sodipodi:nodetypes="cc" + inkscape:original-d="m 36.797679,33.475 c 4.259736,2.23e-4 8.969879,2.23e-4 13.229167,0" + inkscape:path-effect="#path-effect1404-7-6" + inkscape:connector-curvature="0" + id="path1402-8-7" + d="M 36.797679,33.475 H 50.026846" + style="fill:none;fill-rule:evenodd;stroke:#dddddd;stroke-width:0.84519458;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send-8-3)" /> + <path + sodipodi:nodetypes="cc" + inkscape:original-d="m 36.797679,33.475 c 0.03317,4.259607 -0.03362,8.97001 0,13.229166" + inkscape:path-effect="#path-effect1404-7-7-1" + inkscape:connector-curvature="0" + id="path1402-8-0-3" + d="M 36.797679,33.475 V 46.704166" + style="fill:none;fill-rule:evenodd;stroke:#dddddd;stroke-width:0.84519458;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send-8-6-2)" /> + <path + sodipodi:nodetypes="cc" + inkscape:original-d="m 36.797679,33.475 c 4.259736,2.23e-4 8.13389,-13.228083 12.393178,-13.228306" + inkscape:path-effect="#path-effect1404-7-3-9" + inkscape:connector-curvature="0" + id="path1402-8-9-6" + d="M 36.797679,33.475 49.190857,20.246694" + style="fill:none;fill-rule:evenodd;stroke:#dddddd;stroke-width:0.84519458;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send-8-2-27)" /> + <path + sodipodi:nodetypes="cc" + inkscape:original-d="M 36.797679,33.475 C 32.537943,33.475223 27.827801,20.246056 23.568513,20.245833" + inkscape:path-effect="#path-effect1404-7-3-7-6" + inkscape:connector-curvature="0" + id="path1402-8-9-4-1" + d="M 36.797679,33.475 23.568513,20.245833" + style="fill:none;fill-rule:evenodd;stroke:#dddddd;stroke-width:0.84519458;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send-8-2-6-36)" /> + <path + sodipodi:nodetypes="cc" + inkscape:original-d="M 36.797679,33.475 C 32.537943,33.474776 27.827801,46.703943 23.568513,46.704166" + inkscape:path-effect="#path-effect1404-7-3-7-4-8" + inkscape:connector-curvature="0" + id="path1402-8-9-4-4-2" + d="M 36.797679,33.475 23.568513,46.704166" + style="fill:none;fill-rule:evenodd;stroke:#dddddd;stroke-width:0.84519458;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send-8-2-6-3-2)" /> + <path + sodipodi:nodetypes="cc" + inkscape:original-d="m 36.797679,33.475 c 4.259736,-2.23e-4 7.646962,13.228943 11.90625,13.229166" + inkscape:path-effect="#path-effect1404-7-3-4-2" + inkscape:connector-curvature="0" + id="path1402-8-9-3-9" + d="m 36.797679,33.475 11.90625,13.229166" + style="fill:none;fill-rule:evenodd;stroke:#dddddd;stroke-width:0.84519458;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send-8-2-2-7)" /> + </g> </g> </svg> diff --git a/doc/img/logo_no_text.png b/doc/img/logo_no_text.png new file mode 100644 index 0000000000000000000000000000000000000000..66896326cbaa7a5784178fc5845666aabfd9bc91 GIT binary patch literal 12172 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 literal 0 HcmV?d00001 diff --git a/doc/version_from_git.py b/doc/version_from_git.py new file mode 100644 index 00000000..6392ad77 --- /dev/null +++ b/doc/version_from_git.py @@ -0,0 +1,31 @@ +import subprocess + + +def version_number_from_git(tag_prefix='release/', sha_length=10, version_format="{version}.dev{commits}+{sha}"): + def get_released_versions(): + tags = sorted(subprocess.getoutput('git tag').split('\n')) + versions = [t[len(tag_prefix):] for t in tags if t.startswith(tag_prefix)] + return versions + + def tag_from_version(v): + return tag_prefix + v + + def increment_version(v): + parsed_version = [int(i) for i in v.split('.')] + parsed_version[-1] += 1 + return '.'.join(str(i) for i in parsed_version) + + latest_release = get_released_versions()[-1] + commits_since_tag = subprocess.getoutput('git rev-list {}..HEAD --count'.format(tag_from_version(latest_release))) + sha = subprocess.getoutput('git rev-parse HEAD')[:sha_length] + is_dirty = len(subprocess.getoutput("git status --untracked-files=no -s")) > 0 + + if int(commits_since_tag) == 0: + version_string = latest_release + else: + next_version = increment_version(latest_release) + version_string = version_format.format(version=next_version, commits=commits_since_tag, sha=sha) + + if is_dirty: + version_string += ".dirty" + return version_string diff --git a/lbmpy_tests/test_chapman_enskog.py b/lbmpy_tests/test_chapman_enskog.py index 47b2cb37..8cabf4f2 100644 --- a/lbmpy_tests/test_chapman_enskog.py +++ b/lbmpy_tests/test_chapman_enskog.py @@ -1,9 +1,7 @@ import pytest import sympy as sp import functools -from sympy.abc import a, b, x, y, z -from pystencils.fd import expand_diff_products, combine_diff_products, Diff, \ - expand_diff_linear, normalize_diff_order +from pystencils.fd import Diff, normalize_diff_order from pystencils.sympyextensions import multidimensional_sum from lbmpy.chapman_enskog.chapman_enskog_higher_order import determine_higher_order_moments, get_solvability_conditions from lbmpy.chapman_enskog.chapman_enskog_steady_state import SteadyStateChapmanEnskogAnalysisSRT, \ @@ -15,29 +13,6 @@ from lbmpy.creationfunctions import create_lb_method from lbmpy.forcemodels import Guo -def test_derivative_expand_collect(): - original = Diff(x*y*z) - result = combine_diff_products(combine_diff_products(expand_diff_products(original))).expand() - assert original == result - - original = -3 * y * z * Diff(x) + 2 * x * z * Diff(y) - result = expand_diff_products(combine_diff_products(original)).expand() - assert original == result - - original = a + b * Diff(x ** 2 * y * z) - expanded = expand_diff_products(original) - collect_res = combine_diff_products(combine_diff_products(combine_diff_products(expanded))) - assert collect_res == original - - -def test_diff_expand_using_linearity(): - eps = sp.symbols("epsilon") - funcs = [a, b] - test = Diff(eps * Diff(a+b)) - result = expand_diff_linear(test, functions=funcs) - assert result == eps * Diff(Diff(a)) + eps * Diff(Diff(b)) - - def test_srt(): for stencil in ['D2Q9', 'D3Q19', 'D3Q27']: for continuous_eq in (False, True): diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..2d69c7f6 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,37 @@ +[pytest] +python_files = test_*.py *_test.py scenario_*.py +norecursedirs = *.egg-info .git .cache .ipynb_checkpoints htmlcov +addopts = --doctest-modules --durations=20 --cov-config pytest.ini + +[run] +branch = True +source = lbmpy + lbmpy_tests + +omit = doc/* + lbmpy_tests/* + setup.py + conftest.py + +[report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + def __repr__ + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + #raise ValueError + + # Don't complain if non-runnable code isn't run: + if 0: + if False: + if __name__ == .__main__.: + +skip_covered = True +fail_under = 90 + +[html] +directory = coverage_report diff --git a/setup.py b/setup.py index 8e4c3b01..71278613 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,53 @@ import os import sys +import io from setuptools import setup, find_packages -sys.path.insert(0, os.path.abspath('..')) -from custom_pypi_index.pypi_index import get_current_dev_version_from_git +import distutils +from contextlib import redirect_stdout +from importlib import import_module +sys.path.insert(0, os.path.abspath('doc')) +from version_from_git import version_number_from_git + + +quick_tests = [ + 'test_serial_scenarios.test_ldc_mrt', + 'test_force_on_boundary.test_force_on_boundary', +] + + +class SimpleTestRunner(distutils.cmd.Command): + """A custom command to run selected tests""" + + description = 'run some quick tests' + user_options = [] + + @staticmethod + def _run_tests_in_module(test): + """Short test runner function - to work also if py.test is not installed.""" + test = 'lbmpy_tests.' + test + mod, function_name = test.rsplit('.', 1) + if isinstance(mod, str): + mod = import_module(mod) + + func = getattr(mod, function_name) + print(" -> %s in %s" % (function_name, mod.__name__)) + with redirect_stdout(io.StringIO()): + func() + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + """Run command.""" + for test in quick_tests: + self._run_tests_in_module(test) setup(name='lbmpy', - version=get_current_dev_version_from_git(), + version=version_number_from_git(), description='Code Generation for Lattice Boltzmann Methods', author='Martin Bauer', license='AGPLv3', @@ -30,4 +71,7 @@ setup(name='lbmpy', 'interactive': ['pystencils[interactive]', 'scipy', 'scikit-image', 'cython'], 'doc': ['pystencils[doc]'], }, + cmdclass={ + 'quicktest': SimpleTestRunner + } ) -- GitLab