Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve path handling an serialization #27

Merged
merged 2 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,16 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ "ubuntu", "macos", "windows" ]
cmake: [ "3.20", "3.29" ]
pytest: [ "7", "8" ]
cmake: [ "3.20", "3.29" ]
os: [ "ubuntu", "macos", "windows" ]
python: [ "3.8", "3.11", "3.12" ]
bundled: [ false, true ]

name: |
Pytest v${{ matrix.pytest }}
| CMake v${{ matrix.cmake }}
| ${{ matrix.os }}-py${{ matrix.python }}
| bundled: ${{ matrix.bundled }}
v${{ matrix.pytest }}-${{ matrix.cmake }}
[${{ matrix.os }}-py${{ matrix.python }}]
${{ matrix.bundled && '(bundled)' || '' }}

runs-on: "${{ matrix.os }}-latest"

Expand Down Expand Up @@ -64,17 +63,12 @@ jobs:
run: pip install . pytest==${{ matrix.pytest }}.*

- name: Build Example
shell: bash
run: |
cmake --version
cmake \
-DCMAKE_MODULE_PATH:FILEPATH="$(pwd)" \
-S ./example \
-B ./build
cmake -D "CMAKE_MODULE_PATH:FILEPATH=${{ github.workspace }}" -S ./example -B ./build
cmake --build ./build --config Release

- name: Run Tests
shell: bash
working-directory: build
run: ctest -VV
run: ctest -VV -C Release

34 changes: 14 additions & 20 deletions cmake/FindPytest.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -60,41 +60,35 @@ if (Pytest_FOUND AND NOT TARGET Pytest::Pytest)
"LIBRARY_PATH_PREPEND;PYTHON_PATH_PREPEND;ENVIRONMENT;DEPENDS"
)

# Set library path depending on the platform.
# Identify library path environment name depending on the platform.
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
set(LIB_ENV_PATH PATH)
set(_env_sep "\\\;")
set(LIBRARY_ENV_NAME PATH)
elseif(CMAKE_SYSTEM_NAME STREQUAL Darwin)
set(LIB_ENV_PATH DYLD_LIBRARY_PATH)
set(_env_sep ":")
set(LIBRARY_ENV_NAME DYLD_LIBRARY_PATH)
else()
set(LIB_ENV_PATH LD_LIBRARY_PATH)
set(_env_sep ":")
set(LIBRARY_ENV_NAME LD_LIBRARY_PATH)
endif()

# Convert all paths into cmake paths.
cmake_path(CONVERT "$ENV{${LIB_ENV_PATH}}" TO_CMAKE_PATH_LIST libpath)
cmake_path(CONVERT "$ENV{PYTHONPATH}" TO_CMAKE_PATH_LIST pythonpath)
# Sanitize all paths for CMake.
cmake_path(CONVERT "$ENV{${LIBRARY_ENV_NAME}}" TO_CMAKE_PATH_LIST LIBRARY_PATH)
cmake_path(CONVERT "$ENV{PYTHONPATH}" TO_CMAKE_PATH_LIST PYTHON_PATH)

# Prepend input path to environment variables
# Prepend input path to environment variables.
if (_LIBRARY_PATH_PREPEND)
list(REVERSE _LIBRARY_PATH_PREPEND)
foreach (_path ${_LIBRARY_PATH_PREPEND})
set(libpath "${_path}" "${libpath}")
set(LIBRARY_PATH "${_path}" "${LIBRARY_PATH}")
endforeach()
endif()

if (_PYTHON_PATH_PREPEND)
list(REVERSE _PYTHON_PATH_PREPEND)
foreach (_path ${_PYTHON_PATH_PREPEND})
set(pythonpath "${_path}" "${pythonpath}")
set(PYTHON_PATH "${_path}" "${PYTHON_PATH}")
endforeach()
endif()

# Convert list into string.
list(JOIN libpath "${_env_sep}" libpath)
list(JOIN pythonpath "${_env_sep}" pythonpath)

# Default working directory to current build path if none is provided.
if (NOT _WORKING_DIRECTORY)
set(_WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endif()
Expand All @@ -116,9 +110,9 @@ if (Pytest_FOUND AND NOT TARGET Pytest::Pytest)
-D "PYTEST_EXECUTABLE=${PYTEST_EXECUTABLE}"
-D "TEST_GROUP_NAME=${NAME}"
-D "BUNDLE_TESTS=${_BUNDLE_TESTS}"
-D "LIB_ENV_PATH=${LIB_ENV_PATH}"
-D "LIBRARY_PATH=${libpath}"
-D "PYTHON_PATH=${pythonpath}"
-D "LIBRARY_ENV_NAME=${LIBRARY_ENV_NAME}"
-D "LIBRARY_PATH=${LIBRARY_PATH}"
-D "PYTHON_PATH=${PYTHON_PATH}"
-D "TRIM_FROM_NAME=${_TRIM_FROM_NAME}"
-D "WORKING_DIRECTORY=${_WORKING_DIRECTORY}"
-D "ENVIRONMENT=${_ENVIRONMENT}"
Expand Down
23 changes: 16 additions & 7 deletions cmake/PytestAddTests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ if(CMAKE_SCRIPT_MODE_FILE)
# Set Cmake test file to execute each test.
set(_content "")

# Ensure that list environment variables are
# represented as a string on Windows.
string(REPLACE [[;]] [[\;]] LIBRARY_PATH "${LIBRARY_PATH}")
string(REPLACE [[;]] [[\;]] PYTHON_PATH "${PYTHON_PATH}")
cmake_path(CONVERT "${LIBRARY_PATH}" TO_NATIVE_PATH_LIST LIBRARY_PATH)
cmake_path(CONVERT "${PYTHON_PATH}" TO_NATIVE_PATH_LIST PYTHON_PATH)

# Serialize path values separated by semicolons (required on Windows).
macro(encode_value VARIABLE_NAME)
string(REPLACE [[\]] [[\\]] ${VARIABLE_NAME} "${${VARIABLE_NAME}}")
string(REPLACE [[;]] [[\\;]] ${VARIABLE_NAME} "${${VARIABLE_NAME}}")
endmacro()

encode_value(LIBRARY_PATH)
encode_value(PYTHON_PATH)

if (BUNDLE_TESTS)
string(APPEND _content
Expand All @@ -19,7 +26,7 @@ if(CMAKE_SCRIPT_MODE_FILE)
")\n"
"set_tests_properties(\n"
" \"${TEST_GROUP_NAME}\" PROPERTIES\n"
" ENVIRONMENT \"${LIB_ENV_PATH}=${LIBRARY_PATH}\"\n"
" ENVIRONMENT \"${LIBRARY_ENV_NAME}=${LIBRARY_PATH}\"\n"
")\n"
"set_tests_properties(\n"
" \"${TEST_GROUP_NAME}\"\n"
Expand All @@ -29,6 +36,7 @@ if(CMAKE_SCRIPT_MODE_FILE)
)

foreach(env ${ENVIRONMENT})
encode_value(env)
string(APPEND _content
"set_tests_properties(\n"
" \"${TEST_GROUP_NAME}\"\n"
Expand All @@ -40,7 +48,7 @@ if(CMAKE_SCRIPT_MODE_FILE)

else()
# Set environment for collecting tests.
set(ENV{${LIB_ENV_PATH}} "${LIBRARY_PATH}")
set(ENV{${LIBRARY_ENV_NAME}} "${LIBRARY_PATH}")
set(ENV{PYTHONPATH} "${PYTHON_PATH}")
set(ENV{PYTHONWARNINGS} "ignore")

Expand Down Expand Up @@ -99,7 +107,7 @@ if(CMAKE_SCRIPT_MODE_FILE)
")\n"
"set_tests_properties(\n"
" \"${test_name}\" PROPERTIES\n"
" ENVIRONMENT \"${LIB_ENV_PATH}=${LIBRARY_PATH}\"\n"
" ENVIRONMENT \"${LIBRARY_ENV_NAME}=${LIBRARY_PATH}\"\n"
")\n"
"set_tests_properties(\n"
" \"${test_name}\"\n"
Expand All @@ -109,6 +117,7 @@ if(CMAKE_SCRIPT_MODE_FILE)
)

foreach(env ${ENVIRONMENT})
encode_value(env)
string(APPEND _content
"set_tests_properties(\n"
" \"${test_name}\"\n"
Expand Down
15 changes: 14 additions & 1 deletion doc/release/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@
Release Notes
*************

.. release:: Upcoming

.. change:: changed

Updated CMake script to ensure that environment variables are
preserving the Windows-style path syntax when running the tests.

.. seealso:: https://github.com/buddly27/pytest-cmake/issues/22

.. change:: changed

Improve tests.

.. release:: 0.5.2
:date: 2024-05-06

.. change:: fixed
.. change:: fixed

Updated test collection logic to ensure that the 'rootdir' is a
real path. Previously, running the tests from a symlinked directory
Expand Down
4 changes: 4 additions & 0 deletions example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ find_package(Python COMPONENTS Interpreter Development REQUIRED)
set(_py_version ${Python_VERSION_MAJOR}${Python_VERSION_MINOR})
mark_as_advanced(_py_version)

if (WIN32)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE)
endif()

find_package(Boost 1.70.0 COMPONENTS "python${_py_version}" REQUIRED)

if (NOT TARGET Boost::python)
Expand Down
11 changes: 2 additions & 9 deletions example/src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,2 @@
add_library(example MODULE main.cpp)

set_target_properties(example PROPERTIES PREFIX "")

if(WIN32)
set_target_properties(example PROPERTIES SUFFIX ".pyd")
endif()

target_link_libraries(example PUBLIC Boost::python Python::Python)
add_subdirectory(foo)
add_subdirectory(python)
6 changes: 6 additions & 0 deletions example/src/foo/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
add_library(foo SHARED foo.cpp)

target_include_directories(foo
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)
40 changes: 40 additions & 0 deletions example/src/foo/foo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "./foo.h"

#include <cstdlib>
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <stdexcept>

Foo::Foo()
{
const char* settings = std::getenv("FOO_SETTINGS_FILE");
if (settings == nullptr) {
throw std::runtime_error("Environment variable FOO_SETTINGS_FILE is not set.");
}

std::ifstream file(settings);
if (!file.is_open()) {
throw std::runtime_error("Unable to open Foo file.");
}

std::string line;
while (std::getline(file, line)) {
std::istringstream iss(line);
std::string lang, greeting;
if (std::getline(iss, lang, ':') && std::getline(iss, greeting)) {
_map[lang] = greeting;
}
}
file.close();
}

std::string Foo::sayHello(std::string language)
{
if (_map.find(language) == _map.end()) {
throw std::runtime_error("Language not found in Foo file.");
}

return _map[language];
}
18 changes: 18 additions & 0 deletions example/src/foo/foo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#ifndef EXAMPLE_FOO_H
#define EXAMPLE_FOO_H

#include <string>
#include <unordered_map>

class Foo {
public:
Foo();
virtual ~Foo() = default;

std::string sayHello(std::string language);

private:
std::unordered_map<std::string, std::string> _map;
};

#endif // EXAMPLE_FOO_H
32 changes: 32 additions & 0 deletions example/src/python/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
add_library(pyFoo MODULE main.cpp)

set_target_properties(pyFoo
PROPERTIES
PREFIX ""
OUTPUT_NAME foo
)

if(WIN32)
set_target_properties(pyFoo PROPERTIES SUFFIX ".pyd")
endif()

target_link_libraries(pyFoo
PUBLIC
foo
Boost::python
Python::Python
)

if (WIN32)
# As of Python v3.8 and newer, DLLs are no longer searched for in the
# PATH environment variable on Windows. Therefore, it is necessary to
# ensure that they are all located in the same directory.
add_custom_command(
TARGET pyFoo POST_BUILD
COMMAND ${CMAKE_COMMAND}
-E copy_if_different
$<TARGET_FILE:foo>
$<TARGET_FILE_DIR:pyFoo>
COMMAND_EXPAND_LISTS
)
endif()
11 changes: 7 additions & 4 deletions example/src/main.cpp → example/src/python/main.cpp
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
#include <boost/python.hpp>
#include <foo.h>

#include <cstdlib>
#include <string>

std::string greet(std::string name)
{
const char* value = std::getenv("GREETING_WORD");
Foo foo;

const char* value = std::getenv("DEFAULT_LANGUAGE");
if (value != nullptr)
return std::string(value) + ", " + name;
return foo.sayHello(value) + ", " + name;
else
return "bonjour, " + name;
return foo.sayHello("fr") + ", " + name;
}

BOOST_PYTHON_MODULE(example)
BOOST_PYTHON_MODULE(foo)
{
using namespace boost::python;
Py_Initialize();
Expand Down
10 changes: 6 additions & 4 deletions example/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
pytest_discover_tests(
PythonTest
LIBRARY_PATH_PREPEND
$<TARGET_FILE_DIR:example>
$<TARGET_FILE_DIR:foo>
$<TARGET_FILE_DIR:pyFoo>
PYTHON_PATH_PREPEND
$<TARGET_FILE_DIR:example>
$<TARGET_FILE_DIR:pyFoo>
TRIM_FROM_NAME "^test_"
DEPENDS example
DEPENDS foo pyFoo
ENVIRONMENT
"GREETING_WORD=hello"
"DEFAULT_LANGUAGE=en"
"FOO_SETTINGS_FILE=${CMAKE_CURRENT_SOURCE_DIR}/resource/foo.txt"
)
3 changes: 3 additions & 0 deletions example/test/resource/foo.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
en:hello
fr:bonjour
es:hola
16 changes: 13 additions & 3 deletions example/test/subfolder/test_example2.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
# -*- coding: utf-8 -*-
import foo

import example
import pytest


def test_greet_michael():
"""Greet Michael."""
assert example.greet("Michael") == "hello, Michael"
assert foo.greet("Michael") == "hello, Michael"


def test_greet_error(monkeypatch):
"""Impossible to greet when FOO settings is not found."""
monkeypatch.delenv("FOO_SETTINGS_FILE")

with pytest.raises(RuntimeError) as error:
foo.greet("Michael")

assert "Environment variable FOO_SETTINGS_FILE is not set" in str(error.value)
Loading
Loading