Repository: CMakePP/CMinx (language: python)

Issue:
[BUG] Required variables not set after running in CMakeTest test
**Describe the bug**
Some required variables such as `VENV_PYTHON_EXECUTABLE` are set in the CMakeLists.txt and stored in the cache, however CMakeTest uses a separate cache for each generated file so the required variables are not set.

**To Reproduce**
Steps to reproduce the behavior:
1. Write CMakeTest test using CMinx
2. Observe an exception thrown for "No such file or directory"

**Expected behavior**
Required variables should be initialized or retrieved upon a simple `include()` rather than requiring executing `CMakeLists.txt`



Discussion / hints:
I'm confused as to what the issue is. I'm under the impression that CMinx is a stand alone executable. You run the CMinx executable on a repo and then CMinx generates documentation for that repo. So I guess I don't understand how CMakeTest's implementation details affect CMinx's ability to parse files.
The Python executable is stand-alone and works just fine, the problem is the CMake interface to it. In this case the required variables point to the location of the Python interpreter to be used and the program entry point, without them the interface calls `execute_process()` with an empty string as the program name. To be clear the case of CMakeTest causing the issue is just the first instance I've noticed this bug, the issue is in how CMinx's CMake interface stores these variables.
I think I understand. With CMake you usually distribute your installed package with config files (something like `CMinxConfig.cmake`) which help CMake interface with your installed package. When you call `find_package(CMinx)` it'll look for those files and run them. So if you make sure that the config files set the variables you need, then when you do:
```.cmake
find_package(CMinx CONFIG REQUIRED) #IIRC these are the correct options, but it's been a while
execute_process(...)
```
it should work as long as CMinx can be located (CMake will look along the `CMAKE_PREFIX_PATH` for it; by default the search path includes the usual install places like `/usr/local/lib` etc.).

Fix patch:
diff --git a/.github/workflows/cmake_install.yaml b/.github/workflows/cmake_install.yaml
index ba397d18..764db9cc 100644
--- a/.github/workflows/cmake_install.yaml
+++ b/.github/workflows/cmake_install.yaml
@@ -31,10 +31,12 @@ jobs:
       - uses: lukka/get-cmake@v3.20.1
       - name: Build
         run: |
-          cmake -S . -B build -DCMAKE_INSTALL_PREFIX=`pwd`/install
+          cmake -S . -B build -DCMAKE_INSTALL_PREFIX=`pwd`/install -DBUILD_TESTING=ON
           cmake --build build
           cmake --install build
           if ! command -v install/bin/cminx &> /dev/null
           then
               exit 1
           fi
+          cd build
+          ctest --output-on-failure
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8f507bb8..ef8b245f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,9 +1,9 @@
 cmake_minimum_required(VERSION 3.12) #Python3 package locator only added in 3.12
 set (CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${CMAKE_CURRENT_SOURCE_DIR}/cmake")
 
-file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/version.txt" VERSION)
+file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/version.txt" CMINX_VERSION)
 
-project(cminx DESCRIPTION "CMake automatic documentation generator" LANGUAGES NONE VERSION ${VERSION}) #Not using any compiled languages so don't list, removes check for compilers
+project(cminx DESCRIPTION "CMake automatic documentation generator" LANGUAGES NONE VERSION ${CMINX_VERSION}) #Not using any compiled languages so don't list, removes check for compilers
 
 option(BUILD_DOCS "Whether documentation targets should be added" OFF)
 
@@ -26,14 +26,14 @@ set(OUTPUT      "${CMAKE_CURRENT_BINARY_DIR}/build/timestamp")
 #[[[
 # Points to the location of the build virtual environment.
 #]]
-set(VENV "${CMAKE_CURRENT_BINARY_DIR}/virtual-env" CACHE FILEPATH "Directory of the virtual environment")
+set(CMINX_VENV "${CMAKE_CURRENT_BINARY_DIR}/virtual-env" CACHE FILEPATH "Directory of the virtual environment")
 
 #Create virtual environment in the build directory
 message(STATUS "Creating virtual environment for build directory...")
 
-execute_process(COMMAND "${Python3_EXECUTABLE}" "-m" 
-                        "venv" "${VENV}"
-                        RESULT_VARIABLE _venv_status 
+execute_process(COMMAND "${Python3_EXECUTABLE}" "-m"
+                        "venv" "${CMINX_VENV}"
+                        RESULT_VARIABLE _venv_status
                         ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
 
 if (NOT _venv_status EQUAL "0")
@@ -43,23 +43,26 @@ endif()
 #[[[
 # Location of the virtual environment python 3 executable.
 #]]
-set(VENV_PYTHON_EXECUTABLE "${VENV}/bin/python3" CACHE FILEPATH "The location of the virtual environment Python3 executable.")
+set(CMINX_VENV_PYTHON_EXECUTABLE "${CMINX_VENV}/bin/python3" CACHE FILEPATH "The location of the virtual environment Python3 executable.")
 
 #[[[
 # Location of the virtual environment pip.
 #]]
-set(VENV_PIP "${VENV}/bin/pip3" CACHE FILEPATH "The location of the Pip3 to be used for the virtual environment")
+set(CMINX_VENV_PIP "${CMINX_VENV}/bin/pip3" CACHE FILEPATH "The location of the Pip3 to be used for the virtual environment")
 
 #Install needed dependencies
 message(STATUS "Installing build dependencies in virtual environment...")
 
-execute_process(COMMAND "${VENV_PIP}" "install" "pyinstaller") #Install our build tool to our build environment
+execute_process(COMMAND "${CMINX_VENV_PIP}" "install" "pyinstaller") #Install our build tool to our build environment
 
 
 #Install program specific dependencies so that pyinstaller can find them.
-execute_process(COMMAND "${VENV_PIP}" "install" "-r" "${CMAKE_CURRENT_SOURCE_DIR}/requirements.txt")
-
+execute_process(COMMAND "${CMINX_VENV_PIP}" "install" "-r" "${CMAKE_CURRENT_SOURCE_DIR}/requirements.txt")
 
+#Write package config files
+configure_file("${CMAKE_CURRENT_LIST_DIR}/templates/CMinxConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}" @ONLY)
+configure_file("${CMAKE_CURRENT_LIST_DIR}/templates/CMinxConfigVersion.cmake" "${CMAKE_CURRENT_BINARY_DIR}" @ONLY)
+set(CMINX_PACKAGE_SEARCH_PATH "${CMAKE_CURRENT_BINARY_DIR}")
 
 #[[
 # Installation command.
@@ -68,12 +71,13 @@ execute_process(COMMAND "${VENV_PIP}" "install" "-r" "${CMAKE_CURRENT_SOURCE_DIR
 # Third line copies the resultant executable to the installation path.
 #]]
 add_custom_command(OUTPUT ${OUTPUT}
-                   COMMAND ${VENV_PYTHON_EXECUTABLE} "-m" "PyInstaller" "--onefile" "${CMAKE_CURRENT_SOURCE_DIR}/main.py" "--distpath=${CMAKE_CURRENT_BINARY_DIR}/dist" "--runtime-tmpdir=${CMAKE_INSTALL_PREFIX}/bin/.cminx"
+                   COMMAND ${CMINX_VENV_PYTHON_EXECUTABLE} "-m" "PyInstaller" "--onefile" "${CMAKE_CURRENT_SOURCE_DIR}/main.py" "--distpath=${CMAKE_CURRENT_BINARY_DIR}/dist" "--runtime-tmpdir=${CMAKE_INSTALL_PREFIX}/bin/.cminx"
                    COMMAND "sh" "-c" "mkdir -p ${CMAKE_INSTALL_PREFIX}/bin/.cminx" #Creates bin and .cminx if needed
                    COMMAND "sh" "-c" "cp ${CMAKE_CURRENT_BINARY_DIR}/dist/main ${CMAKE_INSTALL_PREFIX}/bin/cminx"
                    COMMAND ${CMAKE_COMMAND} -E touch ${OUTPUT}
                    DEPENDS ${DEPS})
 
+
 add_custom_target(target ALL DEPENDS ${OUTPUT})
 
 install(TARGETS RUNTIME)
@@ -85,5 +89,18 @@ if(BUILD_DOCS)
     cminx_add_docs_target("${CMAKE_CURRENT_SOURCE_DIR}/docs" "${CMAKE_CURRENT_BINARY_DIR}/docs" html)
 endif()
 
+if("${BUILD_TESTING}")
+    message("Building tests")
+    include("${PROJECT_SOURCE_DIR}/cmake/get_cpp.cmake")
+    include("${PROJECT_SOURCE_DIR}/cmake/get_cmake_test.cmake")
+
+    include(cmake_test/cmake_test)
+    include(CTest)
+
+
+    add_subdirectory("tests")
+
+endif()
+
 #Define functions and set source directory
 include("cminx")
diff --git a/cmake/cminx.cmake b/cmake/cminx.cmake
index f2df6bbe..3bb4e906 100644
--- a/cmake/cminx.cmake
+++ b/cmake/cminx.cmake
@@ -14,7 +14,7 @@
 #
 include_guard()
 
-set(CMAKEDOC_SRC "${CMAKE_CURRENT_SOURCE_DIR}" CACHE FILEPATH "Location of CMinx")
+set(CMAKEDOC_SRC "${CMAKE_CURRENT_LIST_DIR}/.." CACHE FILEPATH "Location of CMinx")
 
 
 #[[[
@@ -32,35 +32,31 @@ set(CMAKEDOC_SRC "${CMAKE_CURRENT_SOURCE_DIR}" CACHE FILEPATH "Location of CMinx
 #      titles replaced by the prefix in recursive mode.
 #]]
 function(cminx_gen_rst _cgd_dir _cgd_output)
-        if(${ARGC} GREATER 2)
-            execute_process(
-                COMMAND
-                    "${VENV_PYTHON_EXECUTABLE}"
-                    "${CMAKEDOC_SRC}/main.py"
-                    "${_cgd_dir}"
-                    "-o" "${_cgd_output}"
-                    "-p" "${ARGV2}"
-                WORKING_DIRECTORY "${CMAKEDOC_SRC}"
-                OUTPUT_VARIABLE process_output
-                ERROR_VARIABLE process_err
-                RESULT_VARIABLE process_result)
-        else()
-            execute_process(
-                COMMAND
-                    "${VENV_PYTHON_EXECUTABLE}"
-                    "${CMAKEDOC_SRC}/main.py"
-                    "${_cgd_dir}"
-                    "-o" "${_cgd_output}"
-                WORKING_DIRECTORY "${CMAKEDOC_SRC}"
-                OUTPUT_VARIABLE process_output
-                ERROR_VARIABLE process_err
-                RESULT_VARIABLE process_result
-            )
+        set(_cgr_cminx_options "")
+        if(IS_DIRECTORY "${_cgd_dir}")
+            list(APPEND _cgr_cminx_options "-r")
+        endif()
 
+        if(${ARGC} GREATER 2)
+            list(APPEND _cgr_cminx_options -p ${ARGV2})
         endif()
+
+        execute_process(
+            COMMAND
+                "${CMINX_VENV_PYTHON_EXECUTABLE}"
+                "${CMAKEDOC_SRC}/main.py"
+                "${_cgd_dir}"
+                ${_cgr_cminx_options}
+                "-o" "${_cgd_output}"
+            WORKING_DIRECTORY "${CMAKEDOC_SRC}"
+            OUTPUT_VARIABLE process_output
+            ERROR_VARIABLE process_err
+            RESULT_VARIABLE process_result
+        )
+
 	message("${process_output}")
         if(NOT process_result EQUAL 0)
-            message(FATAL_ERROR "Failed to generate RST. Result code was: ${process_result}. Error output was: ${process_err}")
+            message(FATAL_ERROR "Failed to generate RST. Result code was: ${process_result}. Error output was: ${process_err}. Process output was: ${process_output}")
         endif()
 
 endfunction()
@@ -78,6 +74,6 @@ endfunction()
 # :type _cad_sphinx_target: desc
 #]]
 function(cminx_add_docs_target _cad_doc_dir _cad_output_dir _cad_sphinx_target)
-	add_custom_target(docs COMMAND make "${_cad_sphinx_target}" "BUILDDIR=${_cad_output_dir}" "SPHINXBUILD=${VENV_PYTHON_EXECUTABLE} -m sphinx" WORKING_DIRECTORY "${_cad_doc_dir}")
+	add_custom_target(docs COMMAND make "${_cad_sphinx_target}" "BUILDDIR=${_cad_output_dir}" "SPHINXBUILD=${CMINX_VENV_PYTHON_EXECUTABLE} -m sphinx" WORKING_DIRECTORY "${_cad_doc_dir}")
 endfunction()
 
diff --git a/cmake/get_cpp.cmake b/cmake/get_cpp.cmake
new file mode 100644
index 00000000..ee9b78e1
--- /dev/null
+++ b/cmake/get_cpp.cmake
@@ -0,0 +1,36 @@
+include_guard()
+
+#[[
+# This function encapsulates the process of getting CMakePP using CMake's
+# FetchContent module. We have encapsulated it in a function so we can set
+# the options for its configure step without affecting the options for the
+# parent project's configure step (namely we do not want to build CMakePP's
+# unit tests).
+#]]
+macro(get_cpp)
+    include(cpp/cpp OPTIONAL RESULT_VARIABLE cpp_found)
+    if(NOT cpp_found)
+
+
+
+        # Store whether we are building tests or not, then turn off the tests
+        set(build_testing_old "${BUILD_TESTING}")
+        set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
+        # Download CMakePP and bring it into scope
+        include(FetchContent)
+        FetchContent_Declare(
+             cpp
+             GIT_REPOSITORY https://github.com/CMakePP/CMakePackagingProject
+       )
+       FetchContent_MakeAvailable(cpp)
+
+       # Restore the previous value
+       set(BUILD_TESTING "${build_testing_old}" CACHE BOOL "" FORCE)
+    endif()
+endmacro()
+
+# Call the function we just wrote to get CMakePP
+get_cpp()
+
+# Include CMakePP
+include(cpp/cpp)
diff --git a/templates/CMinxConfig.cmake b/templates/CMinxConfig.cmake
new file mode 100644
index 00000000..30a3fa71
--- /dev/null
+++ b/templates/CMinxConfig.cmake
@@ -0,0 +1,6 @@
+include(cminx)
+
+set(CMINX_VENV "@CMINX_VENV@")
+set(CMINX_VENV_PYTHON_EXECUTABLE "@CMINX_VENV_PYTHON_EXECUTABLE@")
+set(CMINX_VENV_PIP "@CMINX_VENV_PIP@")
+set(CMINX_SRC "@CMINX_SRC@")
diff --git a/templates/CMinxConfigVersion.cmake b/templates/CMinxConfigVersion.cmake
new file mode 100644
index 00000000..86fd2e3f
--- /dev/null
+++ b/templates/CMinxConfigVersion.cmake
@@ -0,0 +1,9 @@
+if(PACKAGE_FIND_NAME STREQUAL "CMinx")
+
+	if(PACKAGE_FIND_VERSION STREQUAL "@CMINX_VERSION@")
+		set(PACKAGE_VERSION_EXACT TRUE)
+		set(PACKAGE_VERSION_COMPATIBLE TRUE)
+	endif()
+	set(PACKAGE_VERSION "@CMINX_VERSION@")
+
+endif()


Test patch:
diff --git a/cmake/get_cmake_test.cmake b/cmake/get_cmake_test.cmake
new file mode 100644
index 00000000..91cdb95c
--- /dev/null
+++ b/cmake/get_cmake_test.cmake
@@ -0,0 +1,36 @@
+include_guard()
+
+#[[
+# This function encapsulates the process of getting CMakeTest using CMake's
+# FetchContent module. We have encapsulated it in a function so we can set
+# the options for its configure step without affecting the options for the
+# parent project's configure step (namely we do not want to build CMakeTest's
+# unit tests).
+#]]
+macro(get_cmake_test)
+    include(cmake_test/cmake_test OPTIONAL RESULT_VARIABLE cmake_test_found)
+    if(NOT cmake_test_found)
+
+
+
+        # Store whether we are building tests or not, then turn off the tests
+        set(build_testing_old "${BUILD_TESTING}")
+        set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
+        # Download CMakeTest and bring it into scope
+        include(FetchContent)
+        FetchContent_Declare(
+             cmake_test
+             GIT_REPOSITORY https://github.com/CMakePP/CMakeTest
+       )
+       FetchContent_MakeAvailable(cmake_test)
+
+       # Restore the previous value
+       set(BUILD_TESTING "${build_testing_old}" CACHE BOOL "" FORCE)
+    endif()
+endmacro()
+
+# Call the function we just wrote to get CMakeTest
+get_cmake_test()
+
+# Include CMakeTest
+include(cmake_test/cmake_test)
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644
index 00000000..619d1682
--- /dev/null
+++ b/tests/CMakeLists.txt
@@ -0,0 +1,10 @@
+
+
+#Sets print length to cached variable. This value is propagated
+#to all tests but can be overriden with subsequent calls or
+#via PRINT_LENGTH option to ct_add_test() and ct_add_section()
+#Added for user convenience.
+set(CT_DEFAULT_PRINT_LENGTH 80 CACHE STRING "The default print length for pass/fail lines. Can be overriden by individual tests.")
+ct_set_print_length("${CT_DEFAULT_PRINT_LENGTH}")
+
+ct_add_dir("cmake" CMAKE_OPTIONS "-DCMINX_PACKAGE_SEARCH_PATH=${CMINX_PACKAGE_SEARCH_PATH}")
diff --git a/tests/cmake/gen_rst.cmake b/tests/cmake/gen_rst.cmake
new file mode 100644
index 00000000..1d5e1ac6
--- /dev/null
+++ b/tests/cmake/gen_rst.cmake
@@ -0,0 +1,73 @@
+find_package(CMinx 1.0.0 PATHS "${CMINX_PACKAGE_SEARCH_PATH}" NO_DEFAULT_PATH REQUIRED CONFIG)
+include(cminx)
+
+set(cwd "${CMAKE_CURRENT_LIST_DIR}")
+
+
+################################################
+# Defines needed macros for setup and teardown #
+################################################
+
+#[[[
+# Sets needed variables for tests to function.
+#]]
+macro(do_setup)
+  set(input_dir "${cwd}/../cmake_input")
+  set(output_dir "${cwd}/output")
+  set(input_file "${input_dir}/example.cmake")
+  set(output_file "${output_dir}/example.rst")
+endmacro()
+
+#[[[
+# Removes output directory after each test.
+# Must be manually called, CMakeTest has no
+# built-in teardown functiionality.
+#]]
+macro(do_teardown)
+  file(REMOVE_RECURSE ${output_dir})
+endmacro()
+
+
+##########################
+# Begin test definitions #
+##########################
+
+
+ct_add_test(NAME test_gen_rst)
+function(${test_gen_rst})
+
+  ct_add_section(NAME test_document)
+  function(${test_document})
+    do_setup()
+    cminx_gen_rst("${input_file}" "${output_dir}")
+
+    ct_assert_file_exists("${output_file}")
+
+    do_teardown()
+
+  endfunction()
+
+  ct_add_section(NAME test_document_prefix)
+  function(${test_document_prefix})
+    do_setup()
+    cminx_gen_rst("${input_file}" "${output_dir}" "input")
+
+    ct_assert_file_exists("${output_file}")
+
+    do_teardown()
+
+  endfunction()
+
+  ct_add_section(NAME test_document_recursive)
+  function(${test_document_recursive})
+    do_setup()
+    cminx_gen_rst("${input_dir}" "${output_dir}")
+
+    ct_assert_file_exists("${output_file}")
+
+    do_teardown()
+
+  endfunction()
+
+
+endfunction()
diff --git a/tests/cmake_input/example.cmake b/tests/cmake_input/example.cmake
new file mode 100644
index 00000000..8649352b
--- /dev/null
+++ b/tests/cmake_input/example.cmake
@@ -0,0 +1,46 @@
+#[[
+# This is a normal block comment and will not
+# be treated as a doccomment.
+#]]
+include_guard()
+
+#[[[
+# This function has very basic documentation.
+#
+# This function's description stays close to idealized formatting and does not do
+# anything fancy.
+#
+# :param person: The person this function says hi to
+# :param me: What my name is
+# :type person: string
+# :type me: string
+#]]
+function(say_hi_to person me)
+    message("Hi ${person}, I am ${me}")
+endfunction()
+
+#[[[
+This macro says hi.
+This documentation uses a differing format,
+but is still processed correctly.
+
+:param person: The person we want to greet.
+:type person: string
+#]]
+macro(macro_say_hi person)
+   message("Hi ${person}")
+endmacro()
+
+
+#[[[
+# This is an example of variable documentation.
+# This variable is a list of string values.
+#]]
+set(MyList "Value" "Value 2")
+
+
+#[[[
+# This is another example of variable documentation.
+# This variable is a string variable.
+#]]
+set(MyString "String")