Repository: doodspav/patomic (language: python)

Issue:
Fix CMake stuff
## Description

Just a general tidy-up of the CMake, with some minor fixes.


Discussion / hints:

Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2ec1eb7be..f284e6903 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,7 +1,14 @@
 cmake_minimum_required(VERSION 3.14)
 
-include(cmake/util/PreventInSourceBuilds.cmake)
+include(cmake/PreventInSourceBuilds.cmake)
 
+
+# ---- Initialize Project ----
+
+# used to support find_package
+set(package_name "patomic")
+
+# create base project
 project(
     patomic
     VERSION 0.5.1
@@ -11,35 +18,43 @@ project(
 )
 
 # don't change include order, OptionVariables checks if project is top level
-include(cmake/util/ProjectIsTopLevel.cmake)
-include(cmake/util/OptionVariables.cmake)
+include(cmake/ProjectIsTopLevel.cmake)
+include(cmake/OptionVariables.cmake)
 
 
 # ---- Declare Library ----
 
-add_library(
-    patomic_patomic
-    ${build_type}
+# target that we can modify (can't modify ALIAS targets)
+# target name should not be the same as ${PROJECT_NAME}, causes add_subdirectory issues
+set(target_name "patomic-patomic")
+add_library(${target_name} ${build_type})
+
+# alias to cause error at configuration time instead of link time if target is missing
+add_library(patomic::patomic ALIAS ${target_name})
+
+# add /include files to target
+# unfortunately can't have CML file in /include
+target_sources(
+    ${target_name} PRIVATE
     # include
     include/patomic/patomic.h
-    # src
-    src/patomic.c
 )
 
-# alias to cause error at configuration time instead of link time if target is missing
-add_library(patomic::patomic ALIAS patomic_patomic)
+# add /src files to target
+add_subdirectory(src)
 
 
 # ---- Generate Build Info Headers ----
 
 # used in export header generated below
 if(NOT PATOMIC_BUILD_SHARED)
-    target_compile_definitions(patomic_patomic PUBLIC PATOMIC_STATIC_DEFINE)
+    target_compile_definitions(${target_name} PUBLIC PATOMIC_STATIC_DEFINE)
 endif()
 
+# generate header file with export macros prefixed with BASE_NAME
 include(GenerateExportHeader)
 generate_export_header(
-    patomic_patomic
+    ${target_name}
     BASE_NAME patomic
     EXPORT_FILE_NAME include/patomic/patomic_export.h
 )
@@ -47,44 +62,55 @@ generate_export_header(
 
 # ---- Library Properties ----
 
+# hide all symbols by default
+# use SameMajorVersion versioning for shared library lookup
 set_target_properties(
-    patomic_patomic PROPERTIES
+    ${target_name} PROPERTIES
     C_VISIBILITY_PRESET hidden
     VISIBILITY_INLINES_HIDDEN YES
     VERSION "${PROJECT_VERSION}"
     SOVERSION "${PROJECT_VERSION_MAJOR}"
-    EXPORT_NAME patomic
-    OUTPUT_NAME patomic
+    EXPORT_NAME "patomic"
+    OUTPUT_NAME "patomic"
 )
 
 # header files generated by CMake
 target_include_directories(
-    patomic_patomic SYSTEM
-    PUBLIC
+    ${target_name} SYSTEM PUBLIC
     "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include>"
 )
 
 # header files from /include
 target_include_directories(
-    patomic_patomic ${warning_guard}
-    PUBLIC
+    ${target_name} ${warning_guard} PUBLIC
     "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
 )
 
-target_compile_features(patomic_patomic PUBLIC c_std_90)
+# require C90 compiler support
+target_compile_features(${target_name} PUBLIC c_std_90)
 
 
 # ---- Install Rules ----
 
 if(NOT CMAKE_SKIP_INSTALL_RULES)
-    include(cmake/util/InstallRules.cmake)
+    include(cmake/InstallRules.cmake)
 endif()
 
 
 # ---- Setup Tests ----
 
 if(PATOMIC_BUILD_TESTING)
+
     # need to enable testing in case BUILD_TESTING is disabled
-    enable_testing()
+    if(PROJECT_IS_TOP_LEVEL)
+        enable_testing()
+    endif()
+
+    # tell unit tests where our files are
+    set(PATOMIC_BINARY_DIR "${PROJECT_BINARY_DIR}")
+    set(PATOMIC_SOURCE_DIR "${PROJECT_SOURCE_DIR}")
+
+    # include test project
     add_subdirectory(test)
+
 endif()
diff --git a/cmake/InstallRules.cmake b/cmake/InstallRules.cmake
new file mode 100644
index 000000000..b8c683c91
--- /dev/null
+++ b/cmake/InstallRules.cmake
@@ -0,0 +1,66 @@
+include(CMakePackageConfigHelpers)
+
+# copy header files to CMAKE_INSTALL_INCLUDEDIR
+install(
+    DIRECTORY
+    "${PROJECT_SOURCE_DIR}/include/"  # our header files
+    "${PROJECT_BINARY_DIR}/include/"  # generated header files
+    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
+    COMPONENT ${package_name}-development
+)
+
+# copy target build output artifacts to OS dependent locations
+# (except includes, that just sets a compiler flag with the path)
+install(
+    TARGETS ${target_name}
+    EXPORT ${package_name}-targets
+    RUNTIME #
+    COMPONENT ${package_name}-runtime
+    LIBRARY #
+    COMPONENT ${package_name}-runtime
+    NAMELINK_COMPONENT ${package_name}-development
+    ARCHIVE #
+    COMPONENT ${package_name}-development
+    INCLUDES #
+    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
+)
+
+# create config file that points to targets file
+configure_file(
+    "${PROJECT_SOURCE_DIR}/cmake/in/patomic-config.cmake.in"
+    "${PROJECT_BINARY_DIR}/cmake/${package_name}-config.cmake"
+    @ONLY
+)
+
+# copy config file for find_package to find
+install(
+    FILES "${PROJECT_BINARY_DIR}/cmake/${package_name}-config.cmake"
+    DESTINATION "${PATOMIC_INSTALL_CMAKEDIR}"
+    COMPONENT ${package_name}-development
+)
+
+# create version file for consumer to check version in CMake
+write_basic_package_version_file(
+    "${package_name}-config-version.cmake"
+    COMPATIBILITY SameMajorVersion  # a.k.a. SemVer
+)
+
+# copy version file for find_package to find for version check
+install(
+    FILES "${PROJECT_BINARY_DIR}/${package_name}-config-version.cmake"
+    DESTINATION "${PATOMIC_INSTALL_CMAKEDIR}"
+    COMPONENT ${package_name}-development
+)
+
+# create targets file included by config file with targets for consumers
+install(
+    EXPORT ${package_name}-targets
+    NAMESPACE patomic::
+    DESTINATION "${PATOMIC_INSTALL_CMAKEDIR}"
+    COMPONENT ${package_name}-development
+)
+
+# support packaging library
+if(PROJECT_IS_TOP_LEVEL)
+    include(CPack)
+endif()
diff --git a/cmake/OptionVariables.cmake b/cmake/OptionVariables.cmake
new file mode 100644
index 000000000..a7480567a
--- /dev/null
+++ b/cmake/OptionVariables.cmake
@@ -0,0 +1,116 @@
+# included further down to avoid interfering with our cache variables
+# include(GNUInstallDirs)
+
+
+# ---- Options Summary ----
+
+# ------------------------------------------------------------------------------------------------
+# | Option                       | Availability  | Default                                       |
+# |==============================|===============|===============================================|
+# | BUILD_SHARED_LIBS            | Top-Level     | OFF                                           |
+# | BUILD_TESTING                | Top-Level     | OFF                                           |
+# | CMAKE_INSTALL_INCLUDEDIR     | Top-Level     | include/${package_name}-${PROJECT_VERSION}    |
+# |------------------------------|---------------|-----------------------------------------------|
+# | PATOMIC_BUILD_SHARED         | Always        | ${BUILD_SHARED_LIBS}                          |
+# | PATOMIC_BUILD_TESTING        | Always        | ${BUILD_TESTING}                              |
+# | PATOMIC_INCLUDES_WITH_SYSTEM | Not Top-Level | ON                                            |
+# | PATOMIC_INSTALL_CMAKEDIR     | Always        | ${CMAKE_INSTALL_LIBDIR}/cmake/${package_name} |
+# ------------------------------------------------------------------------------------------------
+
+
+# ---- Build Shared ----
+
+# Sometimes it's useful to be able to single out a dependency to be built as
+# static or shared, even if obtained from source.
+if(PROJECT_IS_TOP_LEVEL)
+    option(BUILD_SHARED_LIBS "Build shared libs" OFF)
+endif()
+option(
+    PATOMIC_BUILD_SHARED
+    "Override BUILD_SHARED_LIBS for ${package_name} library"
+    ${BUILD_SHARED_LIBS}
+)
+mark_as_advanced(PATOMIC_BUILD_SHARED)
+set(build_type STATIC)
+if(PATOMIC_BUILD_SHARED)
+    set(build_type SHARED)
+endif()
+
+
+# ---- Warning Guard ----
+
+# target_include_directories with SYSTEM modifier will request the compiler to
+# omit warnings from the provided paths, if the compiler supports that.
+# This is to provide a user experience similar to find_package when
+# add_subdirectory or FetchContent is used to consume this project.
+set(warning_guard "")
+if(NOT PROJECT_IS_TOP_LEVEL)
+    option(
+        PATOMIC_INCLUDES_WITH_SYSTEM
+        "Use SYSTEM modifier for ${package_name}'s includes, disabling warnings"
+        ON
+    )
+    mark_as_advanced(PATOMIC_INCLUDES_WITH_SYSTEM)
+    if(PATOMIC_INCLUDES_WITH_SYSTEM)
+        set(warning_guard SYSTEM)
+    endif()
+endif()
+
+
+# ---- Enable Testing ----
+
+# By default tests aren't enabled even with BUILD_TESTING unless the library is
+# built as a top level project.
+# This is in order to cut down on unnecessary compile times, since it's unlikely
+# for users to want to run the tests of their dependencies.
+if(PROJECT_IS_TOP_LEVEL)
+    option(BUILD_TESTING "Build tests" OFF)
+endif()
+option(
+    PATOMIC_BUILD_TESTING
+    "Override BUILD_TESTING for ${package_name} library"
+    ${BUILD_TESTING}
+)
+mark_as_advanced(PATOMIC_BUILD_TESTING)
+
+
+# ---- Install Include Directory ----
+
+# Adds an extra directory to the include path by default, so that when you link
+# against the target, you get `<prefix>/include/<package>-X.Y.Z` added to your
+# include paths rather than `<prefix/include`.
+# This doesn't affect include paths used by consumers of this project.
+# The variable type is STRING rather than PATH, because otherwise passing
+# -DCMAKE_INSTALL_INCLUDEDIR=include on the command line would expand to an
+# absolute path with the base being the current CMake directory, leading to
+# unexpected errors.
+if(PROJECT_IS_TOP_LEVEL)
+    set(
+        CMAKE_INSTALL_INCLUDEDIR "include/${package_name}-${PROJECT_VERSION}"
+        CACHE STRING ""
+    )
+    # marked as advanced in GNUInstallDirs version, so we follow their lead
+    mark_as_advanced(CMAKE_INSTALL_INCLUDEDIR)
+endif()
+
+
+# do not include earlier or we can't set CMAKE_INSTALL_INCLUDEDIR above
+# required for CMAKE_INSTALL_LIBDIR below
+include(GNUInstallDirs)
+
+
+# ---- Install CMake Directory ----
+
+# This allows package maintainers to freely override the installation path for
+# the CMake configs.
+# This doesn't affect include paths used by consumers of this project.
+# The variable type is STRING rather than PATH, because otherwise passing
+# -DPATOMIC_INSTALL_CMAKEDIR=lib/cmake on the command line would expand to an
+# absolute path with the base being the current CMake directory, leading to
+# unexpected errors.
+set(
+    PATOMIC_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/${package_name}"
+    CACHE STRING "CMake package config location relative to the install prefix"
+)
+# depends on CMAKE_INSTALL_LIBDIR which is marked as advanced in GNUInstallDirs
+mark_as_advanced(PATOMIC_INSTALL_CMAKEDIR)
diff --git a/cmake/util/PreventInSourceBuilds.cmake b/cmake/PreventInSourceBuilds.cmake
similarity index 100%
rename from cmake/util/PreventInSourceBuilds.cmake
rename to cmake/PreventInSourceBuilds.cmake
diff --git a/cmake/util/ProjectIsTopLevel.cmake b/cmake/ProjectIsTopLevel.cmake
similarity index 100%
rename from cmake/util/ProjectIsTopLevel.cmake
rename to cmake/ProjectIsTopLevel.cmake
diff --git a/cmake/in/patomic-config.cmake.in b/cmake/in/patomic-config.cmake.in
new file mode 100644
index 000000000..2cdbecf75
--- /dev/null
+++ b/cmake/in/patomic-config.cmake.in
@@ -0,0 +1,12 @@
+# init @ variables before doing anything else
+@PACKAGE_INIT@
+
+# no dependencies or special setup
+
+# we cannot modify an existing IMPORT target
+if(NOT TARGET patomic::patomic)
+
+    # import targets
+    include("${CMAKE_CURRENT_LIST_DIR}/@package_name@-targets.cmake")
+
+endif()
diff --git a/cmake/util/InstallConfig.cmake b/cmake/util/InstallConfig.cmake
deleted file mode 100644
index 30d8d5813..000000000
--- a/cmake/util/InstallConfig.cmake
+++ /dev/null
@@ -1,1 +0,0 @@
-include("${CMAKE_CURRENT_LIST_DIR}/patomicTargets.cmake")
diff --git a/cmake/util/InstallRules.cmake b/cmake/util/InstallRules.cmake
deleted file mode 100644
index 486f3336c..000000000
--- a/cmake/util/InstallRules.cmake
+++ /dev/null
@@ -1,63 +0,0 @@
-include(CMakePackageConfigHelpers)
-include(GNUInstallDirs)
-
-# find_package(<package>) call for consumers to find this project
-set(package patomic)
-
-# copy header files to CMAKE_INSTALL_INCLUDEDIR
-install(
-    DIRECTORY
-    "${PROJECT_SOURCE_DIR}/include/"
-    "${PROJECT_BINARY_DIR}/include/"
-    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
-    COMPONENT patomic_Development
-)
-
-# copy target build output artifacts to OS dependent locations
-install(
-    TARGETS patomic_patomic
-    EXPORT patomicTargets
-    RUNTIME #
-    COMPONENT patomic_Runtime
-    LIBRARY #
-    COMPONENT patomic_Runtime
-    NAMELINK_COMPONENT patomic_Development
-    ARCHIVE #
-    COMPONENT patomic_Development
-    INCLUDES #
-    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
-)
-
-# copy config file for find_package to find
-install(
-    FILES "${PROJECT_SOURCE_DIR}/cmake/util/InstallConfig.cmake"
-    DESTINATION "${PATOMIC_INSTALL_CMAKEDIR}"
-    RENAME "${package}Config.cmake"
-    COMPONENT patomic_Development
-)
-
-# create version file for consumer to check version in CMake
-write_basic_package_version_file(
-    "${package}ConfigVersion.cmake"
-    COMPATIBILITY SameMajorVersion
-)
-
-# copy version file for find_package to find
-install(
-    FILES "${PROJECT_BINARY_DIR}/${package}ConfigVersion.cmake"
-    DESTINATION "${PATOMIC_INSTALL_CMAKEDIR}"
-    COMPONENT patomic_Development
-)
-
-# create core configuration file detailing targets for consumer
-install(
-    EXPORT patomicTargets
-    NAMESPACE patomic::
-    DESTINATION "${PATOMIC_INSTALL_CMAKEDIR}"
-    COMPONENT patomic_Development
-)
-
-# support packaging library
-if(PROJECT_IS_TOP_LEVEL)
-    include(CPack)
-endif()
diff --git a/cmake/util/OptionVariables.cmake b/cmake/util/OptionVariables.cmake
deleted file mode 100644
index 0b5528f08..000000000
--- a/cmake/util/OptionVariables.cmake
+++ /dev/null
@@ -1,78 +0,0 @@
-# ---- Build Shared ----
-
-# Sometimes it's useful to be able to single out a dependency to be built as
-# static or shared, even if obtained from source.
-if(PROJECT_IS_TOP_LEVEL)
-    option(BUILD_SHARED_LIBS "Build shared libs" OFF)
-endif()
-option(
-    PATOMIC_BUILD_SHARED
-    "Override BUILD_SHARED_LIBS for patomic library"
-    ${BUILD_SHARED_LIBS}
-)
-mark_as_advanced(PATOMIC_BUILD_SHARED)
-set(build_type STATIC)
-if(PATOMIC_BUILD_SHARED)
-    set(build_type SHARED)
-endif()
-
-
-# ---- Warning Guard ----
-
-# target_include_directories with SYSTEM modifier will request the compiler to
-# omit warnings from the provided paths, if the compiler supports that.
-# This is to provide a user experience similar to find_package when
-# add_subdirectory or FetchContent is used to consume this project.
-set(warning_guard "")
-if(NOT PROJECT_IS_TOP_LEVEL)
-    option(
-        PATOMIC_INCLUDES_WITH_SYSTEM
-        "Use SYSTEM modifier for patomic's includes, disabling warnings"
-        ON
-    )
-    mark_as_advanced(PATOMIC_INCLUDES_WITH_SYSTEM)
-    if(PATOMIC_INCLUDES_WITH_SYSTEM)
-        set(warning_guard SYSTEM)
-    endif()
-endif()
-
-
-# ---- Enable Testing ----
-
-# By default tests aren't enabled even with BUILD_TESTING unless the library is
-# built as a top level project.
-# This is in order to cut down on unnecessary compile times, since it's unlikely
-# for users to want to run the tests of their dependencies.
-if(PROJECT_IS_TOP_LEVEL)
-    option(BUILD_TESTING "Build tests" OFF)
-endif()
-option(
-    PATOMIC_BUILD_TESTING
-    "Override BUILD_TESTING for patomic library"
-    ${BUILD_TESTING}
-)
-mark_as_advanced(PATOMIC_BUILD_TESTING)
-
-
-# ---- Install Include Directory ----
-
-# Adds an extra directory to the include path by default, so that when you link
-# against the target, you get `<prefix>/include/patomic-X.Y.Z` added to your
-# include paths rather than `<prefix/include`.
-if(PROJECT_IS_TOP_LEVEL)
-    set(
-        CMAKE_INSTALL_INCLUDEDIR "include/patomic-${PROJECT_VERSION}"
-        CACHE PATH ""
-    )
-endif()
-
-
-# ---- Install CMake Directory ----
-
-# This allows package maintainers to freely override the installation path for
-# the CMake configs.
-set(
-    PATOMIC_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/${package}"
-    CACHE PATH "CMake package config location relative to the install prefix"
-)
-mark_as_advanced(PATOMIC_INSTALL_CMAKEDIR)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 000000000..dd861e061
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,4 @@
+target_sources(
+    ${target_name} PRIVATE
+    "patomic.c"
+)


Test patch:
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index a13189af9..5d79d5479 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -16,11 +16,16 @@ include(cmake/OptionVariables.cmake)
 
 include(cmake/ProjectIsTopLevel.cmake)
 
+# need to enable testing ourselves if we're the root project
 if(PROJECT_IS_TOP_LEVEL)
-    find_package(patomic REQUIRED)
     enable_testing()
 endif()
 
+# patomic is not a required dependency
+if(NOT TARGET patomic::patomic)
+    find_package(patomic QUIET)
+endif()
+
 # for Windows: prevent overriding the parent project's compiler/linker settings
 set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
 find_package(GTest REQUIRED)
@@ -29,11 +34,22 @@ include(GoogleTest)
 
 # ---- Declare Tests ----
 
-add_custom_target(patomic_test)
+add_custom_target(patomic-test)
 
-# add all test subdirectories
-add_subdirectory(bt)
-add_subdirectory(ut)
+# only add BTs if patomic target is available
+if(TARGET patomic::patomic)
+    add_subdirectory(bt)
+else()
+    message(STATUS "Skipping binary tests; patomic target not available")
+endif()
+
+# only add UTs if patomic files are available
+# these are currently set by patomic before including this project
+if(PATOMIC_BINARY_DIR AND PATOMIC_SOURCE_DIR)
+   add_subdirectory(ut)
+else()
+    message(STATUS "Skipping unit tests; not building as sub-project of patomic")
+endif()
 
 
 # ---- Support Packaging Library ----
diff --git a/test/bt/CMakeLists.txt b/test/bt/CMakeLists.txt
index 7469ab097..9cccd846e 100644
--- a/test/bt/CMakeLists.txt
+++ b/test/bt/CMakeLists.txt
@@ -1,17 +1,16 @@
-# create_test requires windows_deps_path
 include(../cmake/CreateTest.cmake)
 include(../cmake/WindowsDependenciesPath.cmake)
 
 
 # ---- Setup Tests ----
 
-add_custom_target(patomic_bt)
-add_dependencies(patomic_test patomic_bt)
+add_custom_target(patomic-test-bt)
+add_dependencies(patomic-test patomic-test-bt)
 
-create_test(BT example_add SOURCE example_add.cpp)
-create_test(BT example_sub SOURCE example_sub.cpp)
+create_bt(NAME example_add SOURCE example_add.cpp)
+create_bt(NAME example_sub SOURCE example_sub.cpp)
 
 
 # ---- Windows Path Issues ----
 
-create_test_win_deps_path_file(BT)
+create_test_win_deps_paths_file(BT)
diff --git a/test/cmake/CreateTest.cmake b/test/cmake/CreateTest.cmake
index 97110406b..d30daab9c 100644
--- a/test/cmake/CreateTest.cmake
+++ b/test/cmake/CreateTest.cmake
@@ -1,24 +1,31 @@
 # ---- Create Test ----
 
 # Creates a target to build a test executable and registers it with CTest.
-# Expects a target named patomic_${kind} to exist.
-# E.g. if you call it as create_test(BT ...) then patomic_bt must exist.
-# The target is named patomic_${kind}_${name} but the name of the executable
-# produced is just ${name}.
-# When the executable is installed, it's installed as patomic/${kind}/${name}.
+# Expects a target named patomic-test-${kind} to exist.
+# E.g. if you call it as create_test(BT ...) then patomic-test-bt must exist.
 #
-# create_test(
+# Naming:
+# - created target -> patomic-test-${kind}-${name} (e.g. patomic-test-bt-SomeExample)
+# - executable name -> ${name} (e.g. SomeExample on Unix or SomeExample.exe on Windows)
+# - install directory -> ${CMAKE_INSTALL_TESTDIR}/${kind} (e.g. share/test/bt)
+#
+# Hierarchy:
+# - patomic-test -> base custom target & component (build/install) for all tests
+# - patomic-test-${kind} -> custom target & component (build install) for all tests of a specific kind
+# - patomic-test-${kind}-${name} -> executable target for a single test
+#
+# _create_test(
 #     BT|UT <name>
 #     [INCLUDE <item>...]
 #     [SOURCE <item>...]
 #     [LINK <item>...]
 # )
-function(create_test)
+function(_create_test)
 
     # setup what arguments we expect
 
-    set(all_kinds "BT;UT")  # list we can iterate over
-    set(all_kinds_option "BT|UT")  # string to use in debug message
+    set(all_kinds "BT;UT")  # list of all kinds we can iterate over
+    set(all_kinds_opt_msg "BT|UT")  # string to use in debug message
 
     cmake_parse_arguments(
         "ARG"
@@ -32,9 +39,9 @@ function(create_test)
     # check what test kinds are passed
 
     set(kind "")  # -> "bt"
-    set(name "")  # -> "${ARG_BT}"
-    # we turn kind into a list so that we can check its length (should be 1)
-    # and name is just the last name we process
+    set(name "")  # -> value of "${ARG_BT}"
+    # we turn 'kind' into a list so that we can check its length (which should be 1)
+    # 'name' is just the last name we process (there should only be 1 name)
     foreach(ak IN LISTS all_kinds)
         # if(ARG_BT)
         if (ARG_${ak})
@@ -49,63 +56,101 @@ function(create_test)
 
     # validate arguments
 
+    # setup
     set(args_valid TRUE)
-    set(func_name "create_test")
-    set(LENGTH kind kinds_count)
+    set(func_name "create_test")  # CMAKE_CURRENT_FUNCTION is CMake 3.17+
+    set(LENGTH kind kinds_count)  # expected value is 1
 
+    # go through all possible issues with arguments
     if(kinds_count EQUAL 0)
-        message(WARNING "${all_kinds_option} option needs to be specified when invoking '${func_name}'")
+        message(WARNING "'${all_kinds_opt_msg}' option must be specified invoking '${func_name}'")
         set(args_valid FALSE)
     elseif(kinds_count GREATER 1)
-        message(WARNING "Only a single ${all_kinds_option} option may be specified when invoking '${func_name}'")
+        message(WARNING "Only a single '${all_kinds_opt_msg}' option may be specified when invoking '${func_name}'")
         set(args_valid FALSE)
     elseif(TARGET ${name})
-        message(WARNING "Test name must not be an existing target when invoking '${func_name}', was passed: ${name}")
+        message(WARNING "Test name must not be an existing target when invoking '${func_name}', was passed: '${name}'")
         set(args_valid FALSE)
     elseif("${name}" STREQUAL "")
         message(WARNING "Test name must not be empty when invoking '${func_name}'")
+        set(args_valid FALSE)
     endif()
 
+    # check there are no leftover arguments
     if(DEFINED ARG_UNPARSED_ARGUMENTS)
         message(WARNING "The following arguments were not recognised when invoking '${func_name}': ${ARG_UNPARSED_ARGUMENTS}")
         set(args_valid FALSE)
     endif()
 
-    if (NOT args_valid)
+    # abort if validation failed
+    if(NOT args_valid)
         message(FATAL_ERROR "Aborting '${func_name}' due to invalid arguments")
     endif()
 
 
-    # setup target
+    # create test target
 
-    set(parent_target patomic_${kind})
-    set(target ${parent_target}_${name})
-    set(target_deps patomic::patomic GTest::gtest_main ${ARG_LINK})
-    set(output_name ${name})
+    # setup
+    set(base_target patomic-test)              # patomic-test
+    set(parent_target ${base_target}-${kind})  # patomic-test-bt
+    set(target ${parent_target}-${name})       # patomic-test-bt-SomeExample
 
-    if (NOT "${target}" MATCHES ${PATOMIC_CREATE_TEST_TARGETS_MATCHING})
-        message(DEBUG "Skipping creation of test target ${target} (matches ${PATOMIC_CREATE_TEST_TARGETS_MATCHING})")
-        return()
+    # check target name matches pattern
+    if(NOT "${target}" MATCHES "${PATOMIC_CREATE_TEST_TARGETS_MATCHING}")
+        message(VERBOSE "Skipping creation of test target '${target}' (does not match ${PATOMIC_CREATE_TEST_TARGETS_MATCHING})")
     endif()
 
+    # create target with sources
     add_executable(
         ${target}
-        ${ARG_INCLUDE}
         ${ARG_SOURCE}
     )
 
+    # add include directories
+    target_include_directories(
+        ${target} PRIVATE
+        ${ARG_INCLUDE}
+    )
+
+    # update dependencies list directly because we use it in Windows PATH stuff later
+    if("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.20.0")
+        list(APPEND ARG_LINK GTest::gtest_main)
+        # TODO: this prevents test lookup on Windows; fix once pipeline exists
+        # TODO: see https://github.com/google/googletest/issues/2157
+        # if ("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.23.0")
+        #     list(APPEND ARG_LINK GTest::gmock)
+        # endif()
+    else()
+        list(APPEND ARG_LINK GTest::Main GTest::GTest)
+    endif()
+
+    # link dependencies (all tests use GTest framework)
     target_link_libraries(
-        ${target}
-        PRIVATE
-        ${target_deps}
+        ${target} PRIVATE
+        ${ARG_LINK}
     )
 
-    target_compile_features(${target} PRIVATE cxx_std_14)
+    # require C++14 as minimum
+    target_compile_features(
+        ${target} PRIVATE
+        cxx_std_14
+    )
 
-    set_target_properties(${target} PROPERTIES OUTPUT_NAME ${output_name})
+    # set macro to know which test kind code is part of
+    string(TOUPPER "${kind}" kind_upper)
+    target_compile_definitions(
+        ${target} PRIVATE
+        "PATOMIC_${kind_upper}"
+    )
 
+    # set binary name instead of using default
+    set_target_properties(
+        ${target} PROPERTIES
+        OUTPUT_NAME "${name}"
+    )
 
-    # register tests with CTest
+
+    # register test with GTest/CTest and parent target
 
     # must be run in same directory scope as target
     gtest_add_tests(
@@ -113,84 +158,154 @@ function(create_test)
         TEST_LIST added_tests
     )
 
+    add_dependencies(${parent_target} ${target})
+
 
-    # deal with Windows runtime linker issues for tests (with and without CTest)
+    # deal with Windows runtime linker issues
 
-    if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
+    if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 
         # check we actually care about Windows PATH stuff
-        if (NOT PATOMIC_WINDOWS_SET_CTEST_PATH_ENV AND NOT PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE)
+        if(NOT PATOMIC_WINDOWS_SET_CTEST_PATH_ENV AND
+           NOT PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE)
             return()
         endif()
 
         # get paths to all shared library dependencies (DLLs)
-        windows_deps_path(
-            deps_path
-            ${target_deps}
+        windows_deps_paths(
+            deps_paths
+            ${ARG_LINK}
         )
 
-        # set environment variable for the each test so that CTest works automatically
-        if (deps_path AND PATOMIC_WINDOWS_SET_CTEST_PATH_ENV)
+        # set environment variable for each test so that CTest works
+        if(deps_paths AND PATOMIC_WINDOWS_SET_CTEST_PATH_ENV)
             foreach(test IN LISTS added_tests)
                 set_property(
                     TEST "${test}"
-                    PROPERTY ENVIRONMENT "PATH=${deps_path}"
+                    PROPERTY ENVIRONMENT "PATH=${deps_paths}"
                 )
             endforeach()
         endif()
 
         # make dependencies accessible from parent target
-        if (PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE)
+        # so that we can create single file for all tests in kind
+        if(PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE)
             set_property(
                 TARGET ${parent_target}
-                APPEND PROPERTY WIN_DEP_TARGETS ${target_deps}
+                APPEND PROPERTY WIN_DEPS_TARGETS "${ARG_LINK}"
             )
         endif()
+
     endif()
 
 
     # setup install of target
 
     if(NOT CMAKE_SKIP_INSTALL_RULES)
-        # install as part of patomic_${kind} component
+
+        # install as part of patomic-test-${kind} component
         install(
             TARGETS ${target}
             RUNTIME #
-            COMPONENT "${parent_target}"
-            DESTINATION "${CMAKE_INSTALL_TESTDIR}/patomic/${kind}"
+            COMPONENT ${parent_target}
+            DESTINATION "${CMAKE_INSTALL_TESTDIR}/patomic/${kind}/"
             EXCLUDE_FROM_ALL
         )
 
-        # install as part of patomic_test component
+        # install as part of patomic-test component
         install(
             TARGETS ${target}
             RUNTIME #
-            COMPONENT patomic_test
-            DESTINATION "${CMAKE_INSTALL_TESTDIR}/patomic/${kind}"
+            COMPONENT ${base_target}
+            DESTINATION "${CMAKE_INSTALL_TESTDIR}/patomic/${kind}/"
             EXCLUDE_FROM_ALL
         )
+
     endif()
 
+endfunction()
 
-    # attach to parent target
 
-    add_dependencies(${parent_target} ${target})
+# Creates target patomic-test-bt-${name} corresponding to BT test executable.
+#
+# create_bt(
+#     NAME <name>
+#     [INCLUDE <item>...]
+#     [SOURCE <item>...]
+#     [LINK <item>...]
+# )
+function(create_bt)
+
+    cmake_parse_arguments(
+        "ARG"
+        ""
+        "NAME"
+        "INCLUDE;SOURCE;LINK"
+        ${ARGN}
+    )
+
+    _create_test(
+        ${ARG_UNPARSED_ARGUMENTS}
+        BT      ${ARG_NAME}
+        INCLUDE ${ARG_INCLUDE}
+        SOURCE  ${ARG_SOURCE}
+        LINK
+            patomic::patomic
+            ${ARG_LINK}
+    )
+
+endfunction()
+
+
+# Creates target patomic-test-ut-${name} corresponding to UT test executable.
+#
+# create_ut(
+#     NAME <name>
+#     [INCLUDE <item>...]
+#     [SOURCE <item>...]
+# )
+function(create_ut)
+
+    cmake_parse_arguments(
+        "ARG"
+        ""
+        "NAME"
+        "INCLUDE;SOURCE"
+        ${ARGN}
+    )
+
+    _create_test(
+        ${ARG_UNPARSED_ARGUMENTS}
+        UT      ${ARG_NAME}
+        SOURCE  ${ARG_SOURCE}
+        INCLUDE
+            "$<BUILD_INTERFACE:${PATOMIC_BINARY_DIR}/include>"
+            "$<BUILD_INTERFACE:${PATOMIC_SOURCE_DIR}/include>"
+            "$<BUILD_INTERFACE:${PATOMIC_SOURCE_DIR}/src/include>"
+            ${ARG_INCLUDE}
+    )
+
+    # visibility macros will break UTs on Windows
+    set(target_name patomic-test-ut-${ARG_NAME})
+    target_compile_definitions(${target_name} PRIVATE PATOMIC_STATIC_DEFINE)
 
 endfunction()
 
 
 # ---- Create Test Dependency File ----
 
-# Creates a file containing the output of windows_deps_path for all tests of
+# Creates a file containing the output of windows_deps_paths for all tests of
 # the given kind registered so far.
-# Expects a target named patomic_${kind} to exist
-# E.g. if you call it as create_test_win_deps_path_file(BT) then patomic_bt must
+# The file path will be "${CMAKE_CURRENT_BINARY_DIR}/windows_dependencies_path.txt".
+# Expects a target named patomic-test-${kind} to exist
+# E.g. if you call it as create_test_win_deps_paths_file(BT) then patomic-bt must
 # exist.
+# This function has no effect when not running on Windows.
 #
-# create_test_win_deps_path_file(
+# create_test_win_deps_paths_file(
 #     BT|UT
 # )
-function(create_test_win_deps_path_file ARG_KIND)
+function(create_test_win_deps_paths_file ARG_KIND)
 
     # check we actually want to generate file
 
@@ -201,10 +316,10 @@ function(create_test_win_deps_path_file ARG_KIND)
 
     # check KIND is valid
 
-    set(all_kinds_option "BT|UT")
-    set(func_name "create_test_win_deps_path_file")
+    set(all_kinds_opt_msg "BT|UT")
+    set(func_name "create_test_win_deps_paths_file")
 
-    if (NOT ARG_KIND MATCHES "^(${all_kinds_option})$")
+    if(NOT ARG_KIND MATCHES "^(${all_kinds_option})$")
         message(WARNING "${all_kinds_option} option needs to be specified when invoking '${func_name}'")
         message(FATAL_ERROR "Aborting '${func_name}' due to invalid arguments")
     endif()
@@ -214,19 +329,19 @@ function(create_test_win_deps_path_file ARG_KIND)
 
     # create and install file with dependencies path
 
-    if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
+    if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 
         # get dependencies set by create_test from target
-        get_target_property(dep_targets patomic_${kind} WIN_DEP_TARGETS)
+        get_target_property(dep_targets patomic-test-${kind} WIN_DEP_TARGETS)
         if("${dep_targets}" STREQUAL "dep_targets-NOTFOUND")
-            message(DEBUG "Skipping creation of Windows dependencies PATH file for ${ARG_KIND}; no relevant test targets created")
+            message(VERBOSE "Skipping creation of Windows dependencies PATH file for ${ARG_KIND}; no relevant test targets created")
             return()
         endif()
         list(REMOVE_DUPLICATES dep_targets)
 
         # get paths to all shared library dependencies (DLLs)
-        windows_deps_path(
-            deps_path
+        windows_deps_paths(
+            deps_paths
             ${dep_targets}
         )
 
@@ -234,27 +349,30 @@ function(create_test_win_deps_path_file ARG_KIND)
         set(file_path "${CMAKE_CURRENT_BINARY_DIR}/windows_dependencies_path.txt")
         file(GENERATE
             OUTPUT ${file_path}
-            CONTENT "${deps_path}"
+            CONTENT "${deps_paths}"
         )
 
         # copy file to install location
         if(NOT CMAKE_SKIP_INSTALL_RULES)
-            # install as part of patomic_${kind} component
+
+            # install as part of patomic-test-${kind} component
             install(
                 FILES ${file_path}
-                COMPONENT patomic_${kind}
+                COMPONENT patomic-test-${kind}
                 DESTINATION "${CMAKE_INSTALL_TESTDIR}/patomic/${kind}"
                 EXCLUDE_FROM_ALL
             )
 
-            # install as part of patomic_test component
+            # install as part of patomic-test component
             install(
                 FILES ${file_path}
-                COMPONENT patomic_test
+                COMPONENT patomic-test
                 DESTINATION "${CMAKE_INSTALL_TESTDIR}/patomic/${kind}"
                 EXCLUDE_FROM_ALL
             )
+
         endif()
+
     endif()
 
 endfunction()
diff --git a/test/cmake/OptionVariables.cmake b/test/cmake/OptionVariables.cmake
index e2c8211e4..81a860ad1 100644
--- a/test/cmake/OptionVariables.cmake
+++ b/test/cmake/OptionVariables.cmake
@@ -1,28 +1,46 @@
+# ---- Options Summary ----
+
+# --------------------------------------------------------------------
+# | Option                               | Availability | Default    |
+# |======================================|==============|============|
+# | CMAKE_INSTALL_TESTDIR (unofficial)   | Always       | share/test |
+# |--------------------------------------|--------------|------------|
+# | PATOMIC_CREATE_TEST_TARGETS_MATCHING | Always       | ^(.*)$     |
+# | PATOMIC_WINDOWS_SET_CTEST_PATH_ENV   | Always       | ON         |
+# | PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE | Always       | OFF        |
+# --------------------------------------------------------------------
+
+
 # ---- Test Install Directory ----
 
 # Normally tests would be install in CMAKE_INSTALL_BINDIR by default since
 # they're executables.
 # This is undesirable, so this variable exists to override the install location
 # of test binaries separately.
-# It's not prefixed with patomic_test because it's ok for it to be shared and
+# It's not prefixed with PATOMIC_ because it's ok for it to be shared and
 # overridden by parent projects.
 # Note: this is not an official CMake variable
+# The variable type is STRING rather than PATH, because otherwise passing
+# -DCMAKE_INSTALL_TESTDIR=share/test on the command line would expand to an
+# absolute path with the base being the current CMake directory, leading to
+# unexpected errors.
 set(
     CMAKE_INSTALL_TESTDIR "share/test"
-    CACHE PATH "(unofficial) Default test install location"
+    CACHE STRING "(unofficial) Default test install location"
 )
 
 
 # ---- Test Build Selection ----
 
-# This option provides a way to selectively disable/enable tests based on target
-# name (not suite/case name, or name passed to create_test).
+# This option provides a way to selectively disable/enable building tests based
+# on target name (not suite/case name, or name passed to create_test).
 # Regex must be written according to CMake Regex Specification.
 # By default all test targets are enabled.
 set(
     PATOMIC_CREATE_TEST_TARGETS_MATCHING "^(.*)$"
     CACHE STRING "Only test targets matching regex are created and registered with CTest"
 )
+mark_as_advanced(PATOMIC_CREATE_TEST_TARGETS_MATCHING)
 
 
 # ---- Windows Tests Path ----
@@ -30,8 +48,9 @@ set(
 # By default we set PATH for tests run with CTest on Windows in order to prevent
 # linker errors.
 # Due to limitations in CMake, we can only completely override the PATH, rather
-# than prepend to it.
+# than prepend or append to it.
 # This gives users the option to disable this behaviour.
+# This option has no effect when not running on Windows.
 option(
     PATOMIC_WINDOWS_SET_CTEST_PATH_ENV
     "Set PATH environment variable for tests when run CTest on Windows"
@@ -42,13 +61,16 @@ mark_as_advanced(PATOMIC_WINDOWS_SET_CTEST_PATH_ENV)
 
 # ---- Windows Path File ----
 
-# By default on Windows we generate a file per test kind that contains a string
-# that can be prepended to PATH before running tests, in order to ensure that
+# On Windows we need to set PATH for tests but may not want to have the PATH be
+# completely overridden, like with PATOMIC_WINDOWS_SET_CTEST_PATH_ENV.
+# Instead we can generated a file per test kind that contains a string that can
+# be manually prepended to PATH before running tests, in order to ensure that
 # runtime dependencies can be found.
 # Most of the time we don't need this file (since CTest will take care of that
-# for us), so we don't need to generate it.
+# for us), so we don't generate it by default.
 # Additionally disabled by default because it contains potentially private
 # information about the target platform.
+# This option has no effect when not running on Windows.
 option(
     PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE
     "Create file with PATH environment variables for tests on Windows"
diff --git a/test/cmake/WindowsDependenciesPath.cmake b/test/cmake/WindowsDependenciesPath.cmake
index 4671253a7..c3cedb418 100644
--- a/test/cmake/WindowsDependenciesPath.cmake
+++ b/test/cmake/WindowsDependenciesPath.cmake
@@ -1,33 +1,39 @@
 # ---- Windows Path Prefix ----
 
 # Windows doesn't support rpath, so when linking dynamically the libraries need
-# to either be in the same directory or on PATH.
-# This function sets a variable to a GENERATOR string that may be prepended to
-# path in order to find linked dependencies
+# to either be in the same directory or in PATH.
+# This function sets a variable to a list of GENERATOR strings which resolve to
+# the paths of the linked dependencies.
 # See: https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order
-# Usage: windows_deps_path(<variable> <link-target>...)
-function(windows_deps_path ARG_VAR)
-    if(NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
+# Usage: windows_deps_paths(<variable> <link-target>...)
+function(windows_deps_paths ARG_VAR)
+
+    # check that we're on Windows
+    if(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
         set(${ARG_VAR} "" PARENT_SCOPE)
         return()
     endif()
 
+    # create a list of paths to dependency shared library locations
     set(paths "")
     foreach(target IN LISTS ARGN)
-        # This will fail if passed a link option that isn't a target
-        # This is intentional; don't do that
+        # This will fail if passed a link option that isn't a target.
+        # This is intentional; don't do that.
         # Instead, create an IMPORTED library, and set its target properties
         # such as IMPORTED_LOCATION for the library (.a .so etc.) path and
         # set INTERFACE_INCLUDE_DIRECTORIES to the directory containing any
-        # necessary header files
+        # necessary header files.
         get_target_property(type "${target}" TYPE)
         if(type STREQUAL "SHARED_LIBRARY")
             list(APPEND paths "$<TARGET_FILE_DIR:${target}>")
         endif()
     endforeach()
 
+    # remove duplicates
+    # makes generated file more human readable (if generated)
     list(REMOVE_DUPLICATES paths)
 
+    # concatenate list into string with same format as PATH on Windows
     set(path "")
     set(glue "")
     foreach(p IN LISTS paths)
@@ -35,5 +41,6 @@ function(windows_deps_path ARG_VAR)
         set(glue "\;")  # backslash is important
     endforeach()
 
+    # "return" string with PATH data
     set(${ARG_VAR} "${path}" PARENT_SCOPE)
 endfunction()
diff --git a/test/ut/CMakeLists.txt b/test/ut/CMakeLists.txt
index 20b7cd490..efdd8230e 100644
--- a/test/ut/CMakeLists.txt
+++ b/test/ut/CMakeLists.txt
@@ -1,17 +1,16 @@
-# create_test requires windows_deps_path
 include(../cmake/CreateTest.cmake)
 include(../cmake/WindowsDependenciesPath.cmake)
 
 
 # ---- Setup Tests ----
 
-add_custom_target(patomic_ut)
-add_dependencies(patomic_test patomic_ut)
+add_custom_target(patomic-test-ut)
+add_dependencies(patomic-test patomic-test-ut)
 
-create_test(UT example_add SOURCE example_add.cpp)
-create_test(UT example_sub SOURCE example_sub.cpp)
+create_ut(NAME example_add SOURCE example_add.cpp "${PATOMIC_SOURCE_DIR}/src/patomic.c")
+create_ut(NAME example_sub SOURCE example_sub.cpp "${PATOMIC_SOURCE_DIR}/src/patomic.c")
 
 
 # ---- Windows Path Issues ----
 
-create_test_win_deps_path_file(UT)
+create_test_win_deps_paths_file(UT)