Repository: m-pilia/disptools (language: python)

Issue:
Measure code coverage
Setup code coverage metrics for C and Python.


Discussion / hints:

Fix patch:
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000..f48429a
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,2 @@
+ignore:
+  - test
diff --git a/.gitignore b/.gitignore
index 72578a7..312ed3f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,9 @@ sphinx/readme.rst
 sphinx/_build
 .ycm_extra_conf.py
 .venv
+.ccls
+compile_flags.txt
+compile_commands.json
+.coverage
+coverage.xml
 
diff --git a/.travis.yml b/.travis.yml
index 83c6f17..ef93e6b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,9 +10,36 @@ env:
     - CIBW_BUILD="cp35-manylinux1_x86_64 cp36-manylinux1_x86_64 cp37-manylinux1_x86_64"
 
 script:
+  # Build wheels
   - pip install cibuildwheel==0.12.0
   - cibuildwheel --platform linux --output-dir dist
 
+  # Run Python tests
+  - |
+    docker run -v "$(pwd)":/mnt martinopilia/disptools:1 bash -c '
+    set -xeo pipefail
+    cd /mnt
+    /opt/python/cp37-cp37m/bin/python -m venv /tmp/venv
+    source /tmp/venv/bin/activate
+    pip install codecov==2.0.15 itk SimpleITK scikit-image vtk ply dist/*cp37*.whl
+    python -m coverage run -m unittest discover
+    coverage xml -o coverage.xml
+    deactivate
+    '
+  - whoami | xargs -I{} sudo chown -R {} .
+
+  # Run C tests
+  - mkdir build
+  - cd build
+  - cmake -DDISPTOOLS_COVERAGE=ON ..
+  - make
+  - ctest -VV
+  - cd "${TRAVIS_BUILD_DIR}"
+  - find . -type f -name '*.gcda' -exec gcov {} \;
+
+after_success:
+  - bash <(curl -s https://codecov.io/bash) -X gcov -X coveragepy
+
 deploy:
   provider: pypi
   edge: true
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6ef6ca8..15cf6e6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,10 @@
-cmake_minimum_required(VERSION 3.10)
+cmake_minimum_required(VERSION 3.11)
 
-project(disptools)
+project(disptools LANGUAGES C)
+
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+set(CMAKE_C_EXTENSIONS OFF)
 
 option(DISPTOOLS_DEBUG "Debug build" OFF)
 option(DISPTOOLS_OPT "Enable platform-specific optimisations" OFF)
@@ -13,6 +17,28 @@ option(DISPTOOLS_CUDA_SUPPORT "Build with CUDA support" OFF)
 option(DISPTOOLS_CUDA_ERROR_CHECK "Error check for CUDA calls" ON)
 option(DISPTOOLS_CUDA_ERROR_CHECK_SYNC "Sync device when checking errors" ON)
 option(DISPTOOLS_PYTHON_SUPPORT "Build Python C extension module" OFF)
+option(DISPTOOLS_BUILD_TESTS "Build unit tests" OFF)
+option(DISPTOOLS_COVERAGE "Measure code coverage" OFF)
+
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
+
+if(DISPTOOLS_COVERAGE)
+    if(MSVC)
+        message(FATAL_ERROR "Code coverage not implemented for MSVC")
+    endif()
+
+    set(DISPTOOLS_BUILD_TESTS ON)
+    set(DISPTOOLS_DEBUG ON)
+
+    set(DISPTOOLS_COVERAGE_FLAGS " -coverage -fprofile-arcs -ftest-coverage ")
+
+    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${DISPTOOLS_COVERAGE_FLAGS}")
+    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -coverage -lgcov")
+endif()
+
+if(DISPTOOLS_BUILD_TESTS)
+    enable_testing()
+endif()
 
 set(CMAKE_POSITION_INDEPENDENT_CODE ON)
 set(DISPTOOLS_PYTHON_C_MODULE_NAME "_disptools" CACHE STRING "Name of the C extension module file")
@@ -88,7 +114,7 @@ endif()
 if(MSVC)
     set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W0")
 else()
-    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -std=c99")
+    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")
 endif()
 
 add_subdirectory(src)
@@ -114,6 +140,11 @@ if(DISPTOOLS_CUDA_SUPPORT)
     add_subdirectory(cuda)
 endif()
 
+# Build test suite
+if(DISPTOOLS_BUILD_TESTS)
+    add_subdirectory(test)
+endif()
+
 # Add Python component if requested
 if(DISPTOOLS_PYTHON_SUPPORT)
     set(Python_FIND_VIRTUALENV FIRST)
diff --git a/README.md b/README.md
index 3af2c19..8957bf6 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ Generate displacement fields with known volume changes
 [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/m-pilia/disptools/blob/master/LICENSE)
 [![Appveyor](https://ci.appveyor.com/api/projects/status/github/m-pilia/disptools?svg=true)](https://ci.appveyor.com/project/m-pilia/disptools)
 [![Travis CI Build Status](https://travis-ci.org/m-pilia/disptools.svg?branch=master)](https://travis-ci.org/m-pilia/disptools)
+[![codecov](https://codecov.io/gh/m-pilia/disptools/branch/master/graph/badge.svg)](https://codecov.io/gh/m-pilia/disptools/branch/master)
 [![Downloads](https://pepy.tech/badge/disptools)](https://pepy.tech/project/disptools)
 
 This library provides utilities to generate and manipulate displacement fields with known volume changes. It implements three search-based algorithms for the generation of deformation fields, along with a small collection of utility functions, and provides optional GPU acceleration through a CUDA implementation.
diff --git a/cmake/FindGoogleTest.cmake b/cmake/FindGoogleTest.cmake
new file mode 100644
index 0000000..56f7114
--- /dev/null
+++ b/cmake/FindGoogleTest.cmake
@@ -0,0 +1,17 @@
+# Prevent overriding the parent project's compiler/linker
+# settings on Windows
+set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
+
+include(FetchContent)
+
+fetchcontent_declare(
+    googletest
+    GIT_REPOSITORY https://github.com/google/googletest.git
+    GIT_TAG release-1.8.0
+    )
+
+fetchcontent_getproperties(googletest)
+if(NOT googletest_POPULATED)
+    fetchcontent_populate(googletest)
+    add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} EXCLUDE_FROM_ALL)
+endif()


Test patch:
diff --git a/src/test/test_gradient.c b/src/test/test_gradient.c
deleted file mode 100644
index 89850f8..0000000
--- a/src/test/test_gradient.c
+++ /dev/null
@@ -1,51 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-
-#include "../headers/vtk_io.h"
-#include "../headers/rvf_io.h"
-#include "../headers/jacobian.h"
-#include "../headers/displacement_field_gradient.h"
-
-int main(int argc, char *argv[])
-{
-    (void) argc;
-
-    Image J = read_vtk(argv[1]);
-    Image mask_image = read_vtk(argv[2]);
-    Mask mask = mask_from_image(mask_image);
-
-    print_image_info(J);
-
-    Image field = new_image(3, J.nx, J.ny, J.nz, J.dx, J.dy, J.dz);
-
-    generate_displacement_gradient(
-            field.nx,
-            field.ny,
-            field.nz,
-            field.dx,
-            field.dy,
-            field.dz,
-            (void*) J.data,
-            (void*) mask.data,
-            .99e-3,
-            .2,
-            .4,
-            2.0,
-            0.5,
-            0.3,
-            0.1,
-            1.0,
-            false,
-            10000,
-            (void*) field.data
-            );
-
-    write_vtk(argv[3], J);
-
-    delete_image(&J);
-    delete_image(&mask_image);
-    delete_mask(&mask);
-    delete_image(&field);
-
-    return 0;
-}
diff --git a/src/test/test_greedy.c b/src/test/test_greedy.c
deleted file mode 100644
index 0698af8..0000000
--- a/src/test/test_greedy.c
+++ /dev/null
@@ -1,51 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-
-#include "../headers/vtk_io.h"
-#include "../headers/rvf_io.h"
-#include "../headers/jacobian.h"
-#include "../headers/displacement_field_greedy.h"
-
-int main(int argc, char *argv[])
-{
-    (void) argc;
-
-    Image J = read_vtk(argv[1]);
-    Image mask_image = read_vtk(argv[2]);
-    Mask mask = mask_from_image(mask_image);
-
-    print_image_info(J);
-
-    Image field = new_image(3, J.nx, J.ny, J.nz, J.dx, J.dy, J.dz);
-
-    generate_displacement_greedy(
-            field.nx,
-            field.ny,
-            field.nz,
-            field.dx,
-            field.dy,
-            field.dz,
-            (void*) J.data,
-            (void*) mask.data,
-            .99e-3,
-            .2,
-            .4,
-            2.0,
-            0.5,
-            0.3,
-            0.1,
-            1.0,
-            false,
-            10000,
-            (void*) field.data
-            );
-
-    write_vtk(argv[3], J);
-
-    delete_image(&J);
-    delete_image(&mask_image);
-    delete_mask(&mask);
-    delete_image(&field);
-
-    return 0;
-}
diff --git a/src/test/test_jacobian.c b/src/test/test_jacobian.c
deleted file mode 100644
index d46b4b6..0000000
--- a/src/test/test_jacobian.c
+++ /dev/null
@@ -1,25 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-
-#include "../headers/vtk_io.h"
-#include "../headers/rvf_io.h"
-#include "../headers/jacobian.h"
-
-int main(void)
-{
-    /** Image field = read_rvf("../example.rvf"); */
-    Image field = read_vtk("../example.vtk");
-
-    print_image_info(field);
-
-    Image J = new_image(1, field.nx, field.ny, field.nz, field.dx, field.dy, field.dz);
-
-    jacobian(field, J);
-
-    write_vtk("../jacobian_mine.vtk", J);
-
-    delete_image(&J);
-    delete_image(&field);
-
-    return 0;
-}
diff --git a/src/test/test_matching.c b/src/test/test_matching.c
deleted file mode 100644
index d364d1f..0000000
--- a/src/test/test_matching.c
+++ /dev/null
@@ -1,51 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-
-#include "../headers/vtk_io.h"
-#include "../headers/rvf_io.h"
-#include "../headers/jacobian.h"
-#include "../headers/VolumeMatching3D.h"
-
-int main(int argc, char *argv[])
-{
-    (void) argc;
-
-    Image J = read_vtk(argv[1]);
-    Image mask_image = read_vtk(argv[2]);
-    Mask mask = mask_from_image(mask_image);
-
-    print_image_info(J);
-
-    Image field = new_image(3, J.nx, J.ny, J.nz, J.dx, J.dy, J.dz);
-
-    volume_matching_3d(
-            field.nx,
-            field.ny,
-            field.nz,
-            field.dx,
-            field.dy,
-            field.dz,
-            (void*) J.data,
-            (void*) mask.data,
-            .99e-3,
-            .2,
-            .4,
-            2.0,
-            0.5,
-            0.3,
-            0.1,
-            1.0,
-            false,
-            10000,
-            (void*) field.data
-            );
-
-    write_vtk(argv[3], J);
-
-    delete_image(&J);
-    delete_image(&mask_image);
-    delete_mask(&mask);
-    delete_image(&field);
-
-    return 0;
-}
diff --git a/src/test/test_rvf.c b/src/test/test_rvf.c
deleted file mode 100644
index ce1ee78..0000000
--- a/src/test/test_rvf.c
+++ /dev/null
@@ -1,16 +0,0 @@
-#include <stdio.h>
-
-#include "../headers/rvf_io.h"
-
-int main(void)
-{
-    Image field = read_rvf("../example.rvf");
-
-    print_image_info(field);
-
-    write_rvf("../out.rvf", field);
-
-    delete_image(&field);
-
-    return 0;
-}
diff --git a/src/test/test_vtk.c b/src/test/test_vtk.c
deleted file mode 100644
index 13be637..0000000
--- a/src/test/test_vtk.c
+++ /dev/null
@@ -1,17 +0,0 @@
-#include <stdio.h>
-
-#include "../headers/vtk_io.h"
-
-int main(void)
-{
-    Image field = read_vtk("../example.vtk");
-
-    print_image_info(field);
-    printf("First component of the first voxel: %f\n", _(field, 0, 0, 0, 0));
-
-    write_vtk("../out.vtk", field);
-
-    delete_image(&field);
-
-    return 0;
-}
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
new file mode 100644
index 0000000..1eaf560
--- /dev/null
+++ b/test/CMakeLists.txt
@@ -0,0 +1,44 @@
+project(disptools_test LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+find_package(GoogleTest)
+include(GoogleTest)
+
+if(DISPTOOLS_OMP_SUPPORT)
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
+endif()
+
+if(DISPTOOLS_COVERAGE)
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 ${DISPTOOLS_COVERAGE_FLAGS}")
+endif()
+
+# Headers
+
+include_directories(
+    ${googletest_INCLUDE_DIR}
+    ${PROJECT_SOURCE_DIR}/src/headers
+    )
+
+# Source files
+
+set(TEST_SOURCE_FILES
+    "c/test_jacobian_macros.cpp"
+    )
+
+add_executable(disptools_test ${TEST_SOURCE_FILES})
+
+# Link libraries
+
+target_link_libraries(disptools_test
+    disptools
+    gtest
+    gtest_main
+    pthread
+    )
+
+# Test discovery
+
+gtest_discover_tests(disptools_test)
diff --git a/test/c/test_jacobian_macros.cpp b/test/c/test_jacobian_macros.cpp
new file mode 100644
index 0000000..4380adc
--- /dev/null
+++ b/test/c/test_jacobian_macros.cpp
@@ -0,0 +1,151 @@
+#include "gtest/gtest.h"
+
+#include <vector>
+
+extern "C" {
+#include "disptools.h"
+#include "jacobian_macros.h"
+}
+
+namespace {
+
+class JacobianMacrosTest : public ::testing::Test {
+protected:
+    template<typename T>
+    struct Point {
+        T x;
+        T y;
+        T z;
+        T d;
+    };
+
+    const Point<size_t> size = {5, 6, 7, 3};
+    const Point<FLOATING> spacing = {2.0f, 3.0f, 4.0f, 0.0f};
+    std::vector<FLOATING> data;
+    Image image;
+
+    // Get a component from a voxel of a vector image
+    FLOATING get_component(const Point<size_t> point)
+    {
+        return data[point.d * size.z * size.y * size.x +
+                    point.z * size.y * size.x +
+                    point.y * size.x +
+                    point.x];
+    }
+
+    // Get a second order directional derivative
+    FLOATING derivative2(
+            const FLOATING spacing,
+            const Point<size_t> p,
+            const Point<size_t> d1,
+            const Point<size_t> d2,
+            const size_t component)
+    {
+        const Point<size_t> p0 = {p.x,        p.y,        p.z,        component};
+        const Point<size_t> p1 = {p.x + d1.x, p.y + d1.y, p.z + d1.z, component};
+        const Point<size_t> p2 = {p.x + d2.x, p.y + d2.y, p.z + d2.z, component};
+        return (1.0f / spacing) *
+            (-1.5f * get_component(p0) +
+             +2.0f * get_component(p1) +
+             -0.5f * get_component(p2));
+    }
+
+    void SetUp() override
+    {
+        const size_t data_size = size.z * size.y * size.x * size.d;
+        data.reserve(data_size);
+        for (int i = 0; i < data_size; ++i) {
+            data[i] = static_cast<FLOATING>(i * i);
+        }
+
+        image = {
+            size.d,
+            size.x, size.y, size.z,
+            spacing.x, spacing.y, spacing.z,
+            data.data(),
+        };
+    }
+};
+
+TEST_F(JacobianMacrosTest, dfx_dx_2f)
+{
+    const Point<size_t> p = {2, 1, 3, 0};
+    FLOATING result = dfx_dx_2f(image, p.x, p.y, p.z, 1.0f / spacing.x);
+    FLOATING expected = derivative2(spacing.x, p, {1, 0, 0}, {2, 0, 0}, 0);
+
+    EXPECT_NEAR(result, expected, 1e-6);
+}
+
+TEST_F(JacobianMacrosTest, dfx_dy_2f)
+{
+    const Point<size_t> p = {2, 1, 3, 0};
+    FLOATING result = dfx_dy_2f(image, p.x, p.y, p.z, 1.0f / spacing.x);
+    FLOATING expected = derivative2(spacing.x, p, {0, 1, 0}, {0, 2, 0}, 0);
+
+    EXPECT_NEAR(result, expected, 1e-6);
+}
+
+TEST_F(JacobianMacrosTest, dfx_dz_2f)
+{
+    const Point<size_t> p = {2, 1, 3, 0};
+    FLOATING result = dfx_dz_2f(image, p.x, p.y, p.z, 1.0f / spacing.x);
+    FLOATING expected = derivative2(spacing.x, p, {0, 0, 1}, {0, 0, 2}, 0);
+
+    EXPECT_NEAR(result, expected, 1e-6);
+}
+
+TEST_F(JacobianMacrosTest, dfy_dx_2f)
+{
+    const Point<size_t> p = {2, 1, 3, 0};
+    FLOATING result = dfy_dx_2f(image, p.x, p.y, p.z, 1.0f / spacing.y);
+    FLOATING expected = derivative2(spacing.y, p, {1, 0, 0}, {2, 0, 0}, 1);
+
+    EXPECT_NEAR(result, expected, 1e-6);
+}
+
+TEST_F(JacobianMacrosTest, dfy_dy_2f)
+{
+    const Point<size_t> p = {2, 1, 3, 0};
+    FLOATING result = dfy_dy_2f(image, p.x, p.y, p.z, 1.0f / spacing.y);
+    FLOATING expected = derivative2(spacing.y, p, {0, 1, 0}, {0, 2, 0}, 1);
+
+    EXPECT_NEAR(result, expected, 1e-6);
+}
+
+TEST_F(JacobianMacrosTest, dfy_dz_2f)
+{
+    const Point<size_t> p = {2, 1, 3, 0};
+    FLOATING result = dfy_dz_2f(image, p.x, p.y, p.z, 1.0f / spacing.y);
+    FLOATING expected = derivative2(spacing.y, p, {0, 0, 1}, {0, 0, 2}, 1);
+
+    EXPECT_NEAR(result, expected, 1e-6);
+}
+
+TEST_F(JacobianMacrosTest, dfz_dx_2f)
+{
+    const Point<size_t> p = {2, 1, 3, 0};
+    FLOATING result = dfz_dx_2f(image, p.x, p.y, p.z, 1.0f / spacing.z);
+    FLOATING expected = derivative2(spacing.z, p, {1, 0, 0}, {2, 0, 0}, 2);
+
+    EXPECT_NEAR(result, expected, 1e-6);
+}
+
+TEST_F(JacobianMacrosTest, dfz_dy_2f)
+{
+    const Point<size_t> p = {2, 1, 3, 0};
+    FLOATING result = dfz_dy_2f(image, p.x, p.y, p.z, 1.0f / spacing.z);
+    FLOATING expected = derivative2(spacing.z, p, {0, 1, 0}, {0, 2, 0}, 2);
+
+    EXPECT_NEAR(result, expected, 1e-6);
+}
+
+TEST_F(JacobianMacrosTest, dfz_dz_2f)
+{
+    const Point<size_t> p = {2, 1, 3, 0};
+    FLOATING result = dfz_dz_2f(image, p.x, p.y, p.z, 1.0f / spacing.z);
+    FLOATING expected = derivative2(spacing.z, p, {0, 0, 1}, {0, 0, 2}, 2);
+
+    EXPECT_NEAR(result, expected, 1e-6);
+}
+
+} // anonymous namespace
diff --git a/test/test_displacements.py b/test/test_displacements.py
index 58975fe..83fc528 100644
--- a/test/test_displacements.py
+++ b/test/test_displacements.py
@@ -34,7 +34,7 @@ def test_regularise(self):
 
         img = sitk.GetImageFromArray(perturbed_data)
 
-        res = displacements.regularise(img)
+        res = displacements.regularise(img, epsilon)
 
         a = sitk.GetArrayViewFromImage(res)
         self.assertTrue(np.array_equal(original_data[not_mask], a[not_mask]))