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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 X6Sgi%*So~Pz`)??>gTe~DWM4f)$`M| 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