From fe7f85a7c126fef13232ce2e7fe6f2a0d28c806c Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Tue, 7 Jan 2025 17:41:03 +0100
Subject: [PATCH] Add CONFIG_MODULE parameter to CMake function. Add Tests for
 CMake Integration.

---
 docs/source/usage/project_integration.md      | 15 ++++++--
 mypy.ini                                      |  3 ++
 pyproject.toml                                |  2 +-
 pytest.ini                                    |  1 +
 .../cmake/modules/PystencilsSfg.cmake         | 32 +++++++++-------
 tests/integration/cmake_project/.gitignore    |  1 +
 .../integration/cmake_project/CMakeLists.txt  | 28 ++++++++++++++
 tests/integration/cmake_project/GenTest.py    | 10 +++++
 tests/integration/cmake_project/TestApp.cpp   |  5 +++
 tests/integration/cmake_project/gen_config.py |  9 +++++
 tests/integration/test_cmake.py               | 37 +++++++++++++++++++
 11 files changed, 126 insertions(+), 17 deletions(-)
 create mode 100644 tests/integration/cmake_project/.gitignore
 create mode 100644 tests/integration/cmake_project/CMakeLists.txt
 create mode 100644 tests/integration/cmake_project/GenTest.py
 create mode 100644 tests/integration/cmake_project/TestApp.cpp
 create mode 100644 tests/integration/cmake_project/gen_config.py
 create mode 100644 tests/integration/test_cmake.py

diff --git a/docs/source/usage/project_integration.md b/docs/source/usage/project_integration.md
index 19809a6..de3aa0c 100644
--- a/docs/source/usage/project_integration.md
+++ b/docs/source/usage/project_integration.md
@@ -74,6 +74,7 @@ find_package( PystencilsSfg )
 Make sure to set the `Python_ROOT_DIR` cache variable to point to the correct Python interpreter
 (i.e. the virtual environment you have installed *pystencils-sfg* into).
 
+(cmake_add_generator_scripts)=
 ### Add generator scripts
 
 The primary interaction point in CMake is the function `pystencilssfg_generate_target_sources`,
@@ -85,6 +86,7 @@ pystencilssfg_generate_target_sources( <target>
     [DEPENDS dependency1.py [dependency2.py...]]
     [FILE_EXTENSIONS <header-extension> <impl-extension>]
     [OUTPUT_MODE <standalone|inline|header-only>]
+    [CONFIG_MODULE <path-to-config-module.py>]
 )
 ```
 
@@ -97,6 +99,9 @@ The function takes the following options:
  - `DEPENDS`: A list of dependencies for the generator scripts
  - `FILE_EXTENSION`: The desired extensions for the generated files
  - `OUTPUT_MODE`: Sets the output mode of the code generator; see {any}`SfgConfig.output_mode`.
+ - `CONFIG_MODULE`: Set the configuration module for all scripts registered with this call.
+   If set, this overrides the value of `PystencilsSfg_CONFIG_MODULE`
+   in the current scope (see [](#cmake_set_config_module))
 
 ### Include generated files
 
@@ -110,9 +115,13 @@ path, such that generated header files for a target `<target>` may be included v
 (cmake_set_config_module)=
 ### Set a Configuration Module
 
-To specify a [configuration module](#config_module) for your project,
-set the scoped variable `PystencilsSfg_CONFIG_MODULE` to point at the respective Python file.
-The pystencils-sfg CMake system will then pass that module to each generator script invocation.
+There are two ways of specifying a [configuration module](#config_module) for generator scripts
+registered with CMake:
+- To set a configuration module for scripts registered with a single call to `pystencilssfg_generate_target_sources`,
+  use the `CONFIG_MODULE` function parameter (see [](#cmake_add_generator_scripts)).
+- To set a config module for all generator scripts within the current CMake directory and its subdirectories,
+  set the scoped variable `PystencilsSfg_CONFIG_MODULE` to point at the respective Python file, e.g.
+  `set( PystencilsSfg_CONFIG_MODULE ProjectConfig.py )`.
 
 You might want to populate your configuration module with information about the current
 build setup and environment.
diff --git a/mypy.ini b/mypy.ini
index ca7990b..efef4ea 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -3,3 +3,6 @@ python_version=3.10
 
 [mypy-pystencils.*]
 ignore_missing_imports=true
+
+[mypy-sympy.*]
+ignore_missing_imports=true
diff --git a/pyproject.toml b/pyproject.toml
index f942a18..b787da5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,7 @@ dependencies = [
 ]
 requires-python = ">=3.10"
 readme = "README.md"
-license = { file = "COPYING.txt" }
+license = { file = "LICENSE" }
 dynamic = ["version"]
 
 [project.scripts]
diff --git a/pytest.ini b/pytest.ini
index 91afe80..dd91f52 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -9,5 +9,6 @@ addopts =
     --ignore=tests/generator_scripts/deps
     --ignore=tests/generator_scripts/expected
     --ignore=tests/data
+    --ignore=tests/integration/cmake_project
 
 doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
diff --git a/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake b/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
index c42a251..2765978 100644
--- a/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
+++ b/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
@@ -43,7 +43,7 @@ endfunction()
 
 function(pystencilssfg_generate_target_sources TARGET)
     set(options)
-    set(oneValueArgs OUTPUT_MODE)
+    set(oneValueArgs OUTPUT_MODE CONFIG_MODULE)
     set(multiValueArgs SCRIPTS DEPENDS FILE_EXTENSIONS)
     cmake_parse_arguments(_pssfg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
 
@@ -53,21 +53,27 @@ function(pystencilssfg_generate_target_sources TARGET)
         list(APPEND generatorArgs "--sfg-output-mode=${_pssfg_OUTPUT_MODE}")
     endif()
 
-    if(DEFINED PystencilsSfg_CONFIGURATOR_SCRIPT)
-        message(AUTHOR_WARNING "The variable PystencilsSfg_CONFIGURATOR_SCRIPT is deprecated. Set PystencilsSfg_CONFIG_MODULE instead.")
-        cmake_path(ABSOLUTE_PATH PystencilsSfg_CONFIGURATOR_SCRIPT OUTPUT_VARIABLE configscript)
-        list(APPEND generatorArgs "--sfg-config-module=${configscript}")
-        list(APPEND _pssfg_DEPENDS ${configscript})
-    endif()
-
-    if(DEFINED PystencilsSfg_CONFIG_MODULE)
+    if(DEFINED _pssfg_CONFIG_MODULE)
+        cmake_path(ABSOLUTE_PATH _pssfg_CONFIG_MODULE OUTPUT_VARIABLE config_module)
+        list(APPEND generatorArgs "--sfg-config-module=${config_module}")
+        list(APPEND _pssfg_DEPENDS ${config_module})
+    else()
         if(DEFINED PystencilsSfg_CONFIGURATOR_SCRIPT)
-            message(FATAL_ERROR "At most one of PystencilsSfg_CONFIGURATOR_SCRIPT and PystencilsSfg_CONFIG_MODULE may be set.")
+            message(AUTHOR_WARNING "The variable PystencilsSfg_CONFIGURATOR_SCRIPT is deprecated. Set PystencilsSfg_CONFIG_MODULE instead.")
+            cmake_path(ABSOLUTE_PATH PystencilsSfg_CONFIGURATOR_SCRIPT OUTPUT_VARIABLE configscript)
+            list(APPEND generatorArgs "--sfg-config-module=${configscript}")
+            list(APPEND _pssfg_DEPENDS ${configscript})
         endif()
 
-        cmake_path(ABSOLUTE_PATH PystencilsSfg_CONFIG_MODULE OUTPUT_VARIABLE config_module)
-        list(APPEND generatorArgs "--sfg-config-module=${config_module}")
-        list(APPEND _pssfg_DEPENDS ${config_module})
+        if(DEFINED PystencilsSfg_CONFIG_MODULE)
+            if(DEFINED PystencilsSfg_CONFIGURATOR_SCRIPT)
+                message(FATAL_ERROR "At most one of PystencilsSfg_CONFIGURATOR_SCRIPT and PystencilsSfg_CONFIG_MODULE may be set.")
+            endif()
+
+            cmake_path(ABSOLUTE_PATH PystencilsSfg_CONFIG_MODULE OUTPUT_VARIABLE config_module)
+            list(APPEND generatorArgs "--sfg-config-module=${config_module}")
+            list(APPEND _pssfg_DEPENDS ${config_module})
+        endif()
     endif()
 
     if(DEFINED _pssfg_FILE_EXTENSIONS)
diff --git a/tests/integration/cmake_project/.gitignore b/tests/integration/cmake_project/.gitignore
new file mode 100644
index 0000000..2f4eb1a
--- /dev/null
+++ b/tests/integration/cmake_project/.gitignore
@@ -0,0 +1 @@
+FindPystencilsSfg.cmake
\ No newline at end of file
diff --git a/tests/integration/cmake_project/CMakeLists.txt b/tests/integration/cmake_project/CMakeLists.txt
new file mode 100644
index 0000000..efee912
--- /dev/null
+++ b/tests/integration/cmake_project/CMakeLists.txt
@@ -0,0 +1,28 @@
+cmake_minimum_required( VERSION 3.22 )
+project( sfg_cmake_project_test )
+
+list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
+
+find_package( PystencilsSfg REQUIRED )
+
+set( UseGlobalCfgModule OFF CACHE BOOL "Specify config module globally" )
+set( UseLocalCfgModule OFF CACHE BOOL "Specify config module locally" )
+
+if( $CACHE{UseGlobalCfgModule} )
+    set( PystencilsSfg_CONFIG_MODULE ${CMAKE_CURRENT_SOURCE_DIR}/gen_config.py )
+endif()
+
+add_executable( TestApp TestApp.cpp )
+
+if( $CACHE{UseLocalCfgModule} )
+    pystencilssfg_generate_target_sources(
+        TestApp
+        SCRIPTS GenTest.py
+        CONFIG_MODULE ${CMAKE_CURRENT_SOURCE_DIR}/gen_config.py
+    )
+else()
+    pystencilssfg_generate_target_sources(
+        TestApp
+        SCRIPTS GenTest.py
+    )
+endif()
diff --git a/tests/integration/cmake_project/GenTest.py b/tests/integration/cmake_project/GenTest.py
new file mode 100644
index 0000000..8399e70
--- /dev/null
+++ b/tests/integration/cmake_project/GenTest.py
@@ -0,0 +1,10 @@
+from pystencilssfg import SourceFileGenerator
+
+with SourceFileGenerator() as sfg:
+    sfg.namespace("gen")
+
+    retval = 42 if sfg.context.project_info is None else sfg.context.project_info
+
+    sfg.function("getValue", return_type="int")(
+        f"return {retval};"
+    )
diff --git a/tests/integration/cmake_project/TestApp.cpp b/tests/integration/cmake_project/TestApp.cpp
new file mode 100644
index 0000000..7ceb98b
--- /dev/null
+++ b/tests/integration/cmake_project/TestApp.cpp
@@ -0,0 +1,5 @@
+#include "gen/TestApp/GenTest.hpp"
+
+int main(void) {
+    return int( gen::getValue() );
+}
diff --git a/tests/integration/cmake_project/gen_config.py b/tests/integration/cmake_project/gen_config.py
new file mode 100644
index 0000000..3a62d28
--- /dev/null
+++ b/tests/integration/cmake_project/gen_config.py
@@ -0,0 +1,9 @@
+from pystencilssfg import SfgConfig
+
+
+def configure_sfg(cfg: SfgConfig):
+    cfg.extensions.impl = "c++"
+
+
+def project_info():
+    return 31
diff --git a/tests/integration/test_cmake.py b/tests/integration/test_cmake.py
new file mode 100644
index 0000000..2109158
--- /dev/null
+++ b/tests/integration/test_cmake.py
@@ -0,0 +1,37 @@
+import pytest
+
+import pathlib
+import subprocess
+
+THIS_DIR = pathlib.Path(__file__).parent
+
+CMAKE_PROJECT_DIRNAME = "cmake_project"
+CMAKE_PROJECT_DIR = THIS_DIR / CMAKE_PROJECT_DIRNAME
+
+
+@pytest.mark.parametrize("config_source", [None, "UseGlobalCfgModule", "UseLocalCfgModule"])
+def test_cmake_project(tmp_path, config_source):
+    obtain_find_module_cmd = ["sfg-cli", "cmake", "make-find-module"]
+
+    result = subprocess.run(obtain_find_module_cmd, cwd=CMAKE_PROJECT_DIR)
+    assert result.returncode == 0
+
+    cmake_configure_cmd = ["cmake", "-S", CMAKE_PROJECT_DIR, "-B", str(tmp_path)]
+    if config_source is not None:
+        cmake_configure_cmd.append(f"-D{config_source}=ON")
+    configure_result = subprocess.run(cmake_configure_cmd)
+    assert configure_result.returncode == 0
+
+    cmake_build_cmd = ["cmake", "--build", str(tmp_path), "--target", "TestApp"]
+    build_result = subprocess.run(cmake_build_cmd)
+    assert build_result.returncode == 0
+
+    run_cmd = [str(tmp_path / "TestApp")]
+    run_result = subprocess.run(run_cmd)
+
+    if config_source is not None:
+        assert (tmp_path / "sfg_sources" / "gen" / "TestApp" / "GenTest.c++").exists()
+        assert run_result.returncode == 31
+    else:
+        assert (tmp_path / "sfg_sources" / "gen" / "TestApp" / "GenTest.cpp").exists()
+        assert run_result.returncode == 42
-- 
GitLab