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