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