Skip to content
Open
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
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
option (USE_PYTHON_SABI "Use Python stable ABI" ON)
if (USE_PYTHON_SABI AND CMAKE_VERSION VERSION_GREATER_EQUAL 3.26)
add_compile_definitions(Py_LIMITED_API)
find_package(Python3 REQUIRED COMPONENTS Development.SABIModule)
find_package(Python3 REQUIRED COMPONENTS Interpreter Development.SABIModule)
add_library (Python3::Module ALIAS Python3::SABIModule)
else ()
find_package(Python3 REQUIRED COMPONENTS Development.Module)
find_package(Python3 REQUIRED COMPONENTS Interpreter Development.Module)
endif ()

if (WIN32)
Expand Down
11 changes: 5 additions & 6 deletions pythonfmu/fmi2slave.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .default_experiment import DefaultExperiment
from ._version import __version__ as VERSION
from .enums import Fmi2Type, Fmi2Status, Fmi2Causality, Fmi2Initial, Fmi2Variability
from .log_categories_data import LOG_ALL_CATEGORY, LOG_ALL_DESCRIPTION, LOG_CATEGORIES
from .variables import Boolean, Integer, Real, ScalarVariable, String

ModelOptions = namedtuple("ModelOptions", ["name", "value", "cli"])
Expand All @@ -29,13 +30,11 @@
class Fmi2Slave(ABC):
"""Abstract facade class to execute Python through FMI standard."""

# Dictionary of (category, description) entries
# Dictionary of (category, description) entries; canonical source is
# pythonfmu/log_categories_data.py, shared with the native library.
log_categories: Dict[str, str] = {
"logStatusWarning": "Log messages with fmi2Warning status.",
"logStatusDiscard": "Log messages with fmi2Discard status.",
"logStatusError": "Log messages with fmi2Error status.",
"logStatusFatal": "Log messages with fmi2Fatal status.",
"logAll": "Log all messages."
**{name: description for _, name, description in LOG_CATEGORIES},
LOG_ALL_CATEGORY: LOG_ALL_DESCRIPTION,
}

def __init__(self, **kwargs):
Expand Down
22 changes: 22 additions & 0 deletions pythonfmu/log_categories_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Canonical fmi2Status -> log category mapping.

Single source of truth shared with the native library: at CMake build time,
tools/generate_log_categories_header.py imports this module and generates
LogCategories.hpp (included by src/pythonfmu/Logger.hpp), so the compiled
library and pythonfmu.fmi2slave.Fmi2Slave.log_categories can never diverge.

Edit only this file; the generated C++ header must not be hand-edited.
"""
from typing import List, Tuple

# (fmi2Status C enumerator, category name, description)
LOG_CATEGORIES: List[Tuple[str, str, str]] = [
("fmi2Warning", "logStatusWarning", "Log messages with fmi2Warning status."),
("fmi2Discard", "logStatusDiscard", "Log messages with fmi2Discard status."),
("fmi2Error", "logStatusError", "Log messages with fmi2Error status."),
("fmi2Fatal", "logStatusFatal", "Log messages with fmi2Fatal status."),
]

# Fallback category used for statuses not covered above (e.g. fmi2OK, fmi2Pending).
LOG_ALL_CATEGORY = "logAll"
LOG_ALL_DESCRIPTION = "Log all messages."
44 changes: 44 additions & 0 deletions pythonfmu/tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import math
from pathlib import Path
from unittest.mock import MagicMock

import pytest

Expand Down Expand Up @@ -354,3 +355,46 @@ def test_integration_throw_py_error(tmp_path):

with pytest.raises(Exception):
fmpy.simulate_fmu(str(fmu), stop_time=1.0)


@pytest.mark.integration
@pytest.mark.parametrize(
"categories,expect_logged",
[(("logStatusFatal",), True), (("logStatusError",), False)],
)
def test_integration_throw_py_error_respects_log_categories(tmp_path, categories, expect_logged):
"""A RuntimeError raised in do_step is caught in fmi2DoStep as fmi2Fatal and
logged with category 'logStatusFatal'; it must not be dropped just because it
originates from the internal C++ exception handler (regression test)."""
script_file = Path(__file__).parent / "slaves/PythonSlaveWithException.py"
fmu = FmuBuilder.build_FMU(script_file, dest=tmp_path)
assert fmu.exists()

logger = MagicMock()

callbacks = fmpy.fmi2.fmi2CallbackFunctions()
callbacks.logger = fmpy.fmi2.fmi2CallbackLoggerTYPE(logger)
callbacks.allocateMemory = fmpy.fmi2.fmi2CallbackAllocateMemoryTYPE(fmpy.calloc)
callbacks.freeMemory = fmpy.fmi2.fmi2CallbackFreeMemoryTYPE(fmpy.free)

model_description = fmpy.read_model_description(str(fmu))
unzip_dir = fmpy.extract(str(fmu))

model = fmpy.fmi2.FMU2Slave(
guid=model_description.guid,
unzipDirectory=unzip_dir,
modelIdentifier=model_description.coSimulation.modelIdentifier,
instanceName="instance1")
model.instantiate(callbacks=callbacks)
model.setDebugLogging(True, categories)
model.setupExperiment()
model.enterInitializationMode()
model.exitInitializationMode()

with pytest.raises(Exception):
model.doStep(0.0, 0.1)

fatal_calls = [
c for c in logger.call_args_list if c.args[3] == b"logStatusFatal"
]
assert (len(fatal_calls) == 1) == expect_logged
15 changes: 15 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,25 @@ set(sources
"pythonfmu/PySlaveInstance.cpp"
)

# Generate LogCategories.hpp from the canonical pythonfmu/log_categories_data.py
# so the native library and the Python package share a single source of truth.
set(GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
set(LOG_CATEGORIES_HEADER "${GENERATED_INCLUDE_DIR}/LogCategories.hpp")
add_custom_command(
OUTPUT "${LOG_CATEGORIES_HEADER}"
COMMAND Python3::Interpreter "${CMAKE_SOURCE_DIR}/tools/generate_log_categories_header.py" "${LOG_CATEGORIES_HEADER}"
DEPENDS "${CMAKE_SOURCE_DIR}/pythonfmu/log_categories_data.py" "${CMAKE_SOURCE_DIR}/tools/generate_log_categories_header.py"
COMMENT "Generating LogCategories.hpp from pythonfmu/log_categories_data.py"
VERBATIM
)
add_custom_target(generate_log_categories DEPENDS "${LOG_CATEGORIES_HEADER}")

add_library(pythonfmu-export ${sources} ${headers})
add_dependencies(pythonfmu-export generate_log_categories)
target_compile_features(pythonfmu-export PUBLIC "cxx_std_17")

target_include_directories(pythonfmu-export PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_include_directories(pythonfmu-export PRIVATE "${GENERATED_INCLUDE_DIR}")

target_link_libraries (pythonfmu-export PRIVATE Python3::Module)

Expand Down
20 changes: 18 additions & 2 deletions src/pythonfmu/Logger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#define PYTHONFMU_LOGGER_HPP

#include "fmi/fmi2Functions.h"
#include "LogCategories.hpp"

#include <algorithm>
#include <string>
Expand All @@ -25,10 +26,10 @@ class PyLogger
categories_ = categories;
}

// Logs a message.
// Logs a message, deriving its category from the status (mirrors Fmi2Slave.log in Python).
void log(fmi2Status s, const std::string& message)
{
log(s, "", message);
log(s, defaultCategoryForStatus(s), message);
}

void log(fmi2Status s, const std::string& category, const std::string& message)
Expand All @@ -45,6 +46,21 @@ class PyLogger
private:
bool debugLogging_{false};

// Mirrors the category derivation in Fmi2Slave.log (Python), generated from the
// shared status->category table in LogCategories.hpp so both sides stay in sync.
static std::string defaultCategoryForStatus(fmi2Status s)
{
switch (s) {
#define PYTHONFMU_LOG_CATEGORY(status, name, description) \
case status: \
return name;
PYTHONFMU_LOG_CATEGORIES
#undef PYTHONFMU_LOG_CATEGORY
default:
return PYTHONFMU_LOG_ALL_CATEGORY;
}
}

protected:
std::string instanceName_;
std::vector<std::string> categories_;
Expand Down
3 changes: 0 additions & 3 deletions src/pythonfmu/fmi2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,6 @@ fmi2Status fmi2GetDirectionalDerivative(
{
static_cast<Fmi2Component*>(c)->logger->log(
fmi2Error,
"cppfmu",
"FMI function not supported: fmi2GetDirectionalDerivative");
return fmi2Error;
}
Expand All @@ -523,7 +522,6 @@ fmi2Status fmi2SetRealInputDerivatives(
{
static_cast<Fmi2Component*>(c)->logger->log(
fmi2Error,
"cppfmu",
"FMI function not supported: fmi2SetRealInputDerivatives");
return fmi2Error;
}
Expand All @@ -537,7 +535,6 @@ fmi2Status fmi2GetRealOutputDerivatives(
{
static_cast<Fmi2Component*>(c)->logger->log(
fmi2Error,
"cppfmu",
"FMI function not supported: fmiGetRealOutputDerivatives");
return fmi2Error;
}
Expand Down
44 changes: 44 additions & 0 deletions tools/generate_log_categories_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Generate the native LogCategories.hpp from pythonfmu/log_categories_data.py.

Invoked automatically by CMake at build time (see src/CMakeLists.txt) so the
native library and the Python package always agree on the log category table.

Usage: generate_log_categories_header.py <output_header_path>
"""
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from pythonfmu.log_categories_data import ( # noqa: E402
LOG_ALL_CATEGORY,
LOG_ALL_DESCRIPTION,
LOG_CATEGORIES,
)


def main(output_path: str) -> None:
entries = " \\\n".join(
f' PYTHONFMU_LOG_CATEGORY({status}, "{name}", "{description}")'
for status, name, description in LOG_CATEGORIES
)
content = f"""// Auto-generated by tools/generate_log_categories_header.py from
// pythonfmu/log_categories_data.py -- do not edit by hand.
#ifndef PYTHONFMU_LOGCATEGORIES_HPP
#define PYTHONFMU_LOGCATEGORIES_HPP

#define PYTHONFMU_LOG_CATEGORIES \\
{entries}

#define PYTHONFMU_LOG_ALL_CATEGORY "{LOG_ALL_CATEGORY}"
#define PYTHONFMU_LOG_ALL_DESCRIPTION "{LOG_ALL_DESCRIPTION}"

#endif // PYTHONFMU_LOGCATEGORIES_HPP
"""
out = Path(output_path)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(content)


if __name__ == "__main__":
main(sys.argv[1])
Loading