diff --git a/.github/workflows/unix.yml b/.github/workflows/unix.yml index 3e93f53..700f73d 100644 --- a/.github/workflows/unix.yml +++ b/.github/workflows/unix.yml @@ -2,7 +2,8 @@ name: Unix Build on: push: - branches: [ master ] + branches: [ doc-with-doxygen-and-sphinx ] # Temporary, restore next line before merging MR + # branches: [ master ] pull_request: branches: [ master ] @@ -82,11 +83,16 @@ jobs: DEBIAN_FRONTEND: noninteractive UBSAN_OPTIONS: halt_on_error=1:abort_on_error=1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: seanmiddleditch/gha-setup-ninja@master if: matrix.config.os == 'macos-latest' + - name: Install doxygen 🔧 + uses: ssciwr/doxygen-install@v1 + with: + version: "1.13.2" + # ==== INSTALL ==== - name: Update APT if: matrix.config.os != 'macos-latest' @@ -141,21 +147,28 @@ jobs: steps: - name: Checkout 🛎️ uses: actions/checkout@v4 + - name: Set up Python 3.13 🔧 uses: actions/setup-python@v5 with: python-version: 3.13 + - name: Install doxygen 🔧 uses: ssciwr/doxygen-install@v1 with: version: "1.13.2" + - name: Install Python dependencies ⚙️ run: pip install -r docs/requirements.txt + - name: Generate HTML documentation 🏗️ run: | - mkdir -p public/ - - - name: Deploy documentation 🚀 + cd docs + doxygen + sphinx-build -M html . _build + mv _build ../public + + - name: Deploy documentation onto GitHub Pages 🚀 if: github.ref == 'refs/heads/master' uses: peaceiris/actions-gh-pages@v3 with: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 896a2a6..95eb2bf 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -31,8 +31,15 @@ jobs: cmake-opt: -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - uses: seanmiddleditch/gha-setup-ninja@master + + - name: Install doxygen 🔧 + uses: ssciwr/doxygen-install@v1 + with: + version: "1.13.2" + - uses: ilammy/msvc-dev-cmd@v1 - name: Build diff --git a/CMakeLists.txt b/CMakeLists.txt index 5de2e11..7e8112f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ endif() add_executable(utl-test ${utl-test-files}) target_link_libraries(utl-test utl gtest gmock gtest_main) if (NOT MSVC) - target_compile_options(utl-test PRIVATE -Wall -Wextra -Werror) + target_compile_options(utl-test PRIVATE -Wall -Wextra -Werror -Wno-invalid-token-paste) endif() add_subdirectory ("docs") diff --git a/docs/Doxyfile b/docs/Doxyfile index 9c058c0..29196f9 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -41,7 +41,7 @@ DOXYFILE_ENCODING = UTF-8 # project for which the documentation is generated. This name is used in the # title of most generated pages and in a few other places. -PROJECT_NAME = "Motis utl module" +PROJECT_NAME = "MOTIS utl module" # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version diff --git a/docs/README.md b/docs/README.md index 9b3b8fc..26718db 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,11 @@ Directly with `doxygen`: doxygen ## Generating Sphinx documentation -From the root directory: +From the root directory, with auto-reload: sphinx-autobuild docs docs/_build/html --open-browser + +Only generating the documentation: + + cd docs + sphinx-build -M html . _build diff --git a/docs/conf.py b/docs/conf.py index 420d595..72e23cf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,9 +6,9 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'Motis utl module' -author = 'The Motis project developers' -copyright = '2025, The Motis project developers' +project = 'MOTIS utl module' +author = 'The MOTIS project developers' +copyright = '2025, The MOTIS project developers' html_show_copyright = False # -- General configuration --------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 6ddf094..350fca7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,10 +1,10 @@ -Motis utl module documentation +MOTIS utl module documentation ============================== MOTIS is an open-source software platform for efficient planning and routing in multi-modal transportation systems. GitHub main repository: https://github.com/motis-project/motis -This is the documentation for the `utl` (utility) module. +This is the documentation for the **utl** (utility) module. .. Table of contents @@ -17,12 +17,35 @@ This is the documentation for the `utl` (utility) module. Logging ------- -Logs can be produced using the `log()` `struct`:: +The simplest way to produce log lines is to use the ``logF()`` macro:: - utl::log() << "[" << utl::log::str["info"] << "]" \ - << "[" << utl::time() << "]" \ - << "[" << FILE_NAME << ":" << __LINE__ << "]" \ - << " Some message" + logF(info, "Simple message"); -.. doxygenstruct:: utl::log - :members: +The following log levels are supported: + +debug + Messages that contain information only useful when debugging MOTIS + +info + Important information about a normal behavior of the program + +error + Details on an abnormal behavior of the application + +Advanced usage +^^^^^^^^^^^^^^ + +By default, ``logF()`` inserts the current filename & linenumber in the log line. +However, you can use ``log()`` to specify your own **context** :: + + log(info, "http.get.resource", "Details"); + +You can also insert variables in the message by using ``{}`` and passing them as extra arguments:: + + logF(info, "String={} Int={}", "Hello", 42); + +API details +^^^^^^^^^^^ +Under the hood, the ``log()`` & ``logF()`` macros use the ``utl::log()`` function: + +.. doxygenfunction:: utl::log diff --git a/include/utl/logging.h b/include/utl/logging.h index dba3f2a..3f09615 100644 --- a/include/utl/logging.h +++ b/include/utl/logging.h @@ -5,75 +5,84 @@ #else #include -#include -#include +#include #include -#include +#include #include -#ifdef _MSC_VER -#define gmt(a, b) gmtime_s(b, a) -#else -#define gmt(a, b) gmtime_r(a, b) -#endif +#include "fmt/core.h" +#include "fmt/ostream.h" -#define FILE_NAME \ - (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) +#define STRINGIFY(x) STRINGIFY_(x) +#define STRINGIFY_(x) #x -#define uLOG(lvl) \ - utl::log() << "[" << utl::log::str[lvl] << "]" \ - << "[" << utl::time() << "]" \ - << "[" << FILE_NAME << ":" << __LINE__ << "]" \ - << " " +#define FILE_AND_LINE (__FILE__ ":" STRINGIFY(__LINE__)) +#if defined(_WIN32) +#define FILE_AND_LINE_SHORT \ + (strrchr(FILE_AND_LINE, '\\') ? strrchr(FILE_AND_LINE, '\\') + 1 \ + : FILE_AND_LINE) +#else +#define FILE_AND_LINE_SHORT \ + (strrchr(FILE_AND_LINE, '/') ? strrchr(FILE_AND_LINE, '/') + 1 \ + : FILE_AND_LINE) +#endif namespace utl { -/** - Entrypoint for all Motis logs -*/ -struct log { - log() = default; - - log(log const&) = delete; - log& operator=(log const&) = delete; - - log(log&&) = default; - log& operator=(log&&) = default; +enum class log_level { debug, info, error }; - template - friend log&& operator<<(log&& l, T&& t) { - std::clog << std::forward(t); - return std::move(l); +constexpr char const* to_str(log_level const level) { + switch (level) { + case log_level::debug: return "debug"; + case log_level::info: return "info"; + case log_level::error: return "error"; } + return ""; +} - ~log() { std::clog << std::endl; } - - static constexpr const char* const str[]{"emrg", "alrt", "crit", "erro", - "warn", "note", "info", "debg"}; -}; +static log_level s_verbosity; -enum log_level { emrg, alrt, crit, err, warn, notice, info, debug }; +inline std::string now() { + using clock = std::chrono::system_clock; + auto const now = clock::to_time_t(clock::now()); + struct tm tmp {}; +#if _MSC_VER >= 1400 + gmtime_s(&tmp, &now); +#else + gmtime_r(&now, &tmp); +#endif -/** - Format a timestamp as an ISO 8601 string -*/ -inline std::string time(time_t const t) { - char buf[sizeof "2011-10-08t07:07:09z-0430"]; - struct tm result {}; - gmt(&t, &result); - strftime(buf, sizeof buf, "%FT%TZ%z", &result); - return buf; + std::stringstream ss; + ss << std::put_time(&tmp, "%FT%TZ"); + return ss.str(); } -/** - Format the current time as an ISO 8601 string -*/ -inline std::string time() { - time_t now; - std::time(&now); - return time(now); +/// Produce a new log line at the given `level`, with the given prefix `ctx` and +/// message +template +void log(log_level const level, char const* ctx, + fmt::format_string fmt_str, Args&&... args) { + if (level >= ::utl::s_verbosity) { + fmt::print(std::clog, "{time} [{level}] [{ctx}] {msg}\n", + fmt::arg("time", now()), fmt::arg("level", to_str(level)), + fmt::arg("ctx", ctx), + fmt::arg("msg", fmt::format(fmt::runtime(fmt_str), + std::forward(args)...))); + } } } // namespace utl -#endif +/** + * Shorthand to invoke utl::log without specifying the namespace + */ +#define log(level, ctx, fmt_str, ...) \ + utl::log(utl::log_level::##level, ctx, fmt_str, __VA_ARGS__) + +/** + * Invoke utl::log using the current C++ filename & line number as ctx + */ +#define logF(level, fmt_str, ...) \ + log(level, FILE_AND_LINE_SHORT, fmt_str, __VA_ARGS__) + +#endif // LOGGING_HEADER diff --git a/include/utl/parallel_for.h b/include/utl/parallel_for.h index 5d75804..15053e6 100644 --- a/include/utl/parallel_for.h +++ b/include/utl/parallel_for.h @@ -160,7 +160,7 @@ inline errors_t parallel_for( jobs.size(), [&](auto const idx) { if (idx % mod == 0) { - uLOG(info) << desc << " " << idx << "/" << jobs.size(); + logF(info, "{} {}/{}", desc, idx, jobs.size()); } func(jobs[idx]); }, diff --git a/src/timer.cc b/src/timer.cc index 850348b..bd83fa3 100644 --- a/src/timer.cc +++ b/src/timer.cc @@ -6,7 +6,7 @@ namespace utl { scoped_timer::scoped_timer(std::string name) : name_{std::move(name)}, start_{std::chrono::steady_clock::now()} { - uLOG(info) << "[" << name_ << "] starting"; + logF(info, "[{}] starting", name); } scoped_timer::~scoped_timer() { @@ -15,8 +15,7 @@ scoped_timer::~scoped_timer() { double t = static_cast(duration_cast(stop - start_).count()) / 1000.0; - uLOG(info) << "[" << name_ << "] finished" - << " (" << t << "ms)"; + logF(info, "[{}] finished ({}ms)", name_, t); } void scoped_timer::print(std::string_view const message) const { @@ -25,12 +24,12 @@ void scoped_timer::print(std::string_view const message) const { double const t = static_cast(duration_cast(stop - start_).count()) / 1000.0; - uLOG(info) << "[" << name_ << "] " << message << " (" << t << "ms)"; + logF(info, "[{}] {} ({}ms)", name_, message, t); } manual_timer::manual_timer(std::string name) : name_{std::move(name)}, start_{std::chrono::steady_clock::now()} { - uLOG(info) << "[" << name_ << "] starting"; + logF(info, "[{}] starting", name_); } void manual_timer::stop_and_print() const { @@ -39,8 +38,7 @@ void manual_timer::stop_and_print() const { double t = static_cast(duration_cast(stop - start_).count()) / 1000.0; - uLOG(info) << "[" << name_ << "] finished" - << " (" << t << "ms)"; + logF(info, "[{}] finished ({}ms)", name_, t); } void manual_timer::print(std::string_view const message) const { @@ -49,7 +47,7 @@ void manual_timer::print(std::string_view const message) const { double const t = static_cast(duration_cast(stop - start_).count()) / 1000.0; - uLOG(info) << "[" << name_ << "] " << message << " (" << t << "ms)"; + logF(info, "[{}] {} ({}ms)", name_, message, t); } } // namespace utl \ No newline at end of file diff --git a/test/logging_test.cc b/test/logging_test.cc new file mode 100644 index 0000000..5e9f683 --- /dev/null +++ b/test/logging_test.cc @@ -0,0 +1,30 @@ +#include +#include + +#include "utl/logging.h" + +using ::testing::MatchesRegex; + +TEST(log, basic_usage) { + testing::internal::CaptureStderr(); + logF(info, "Simple message"); + EXPECT_THAT( + testing::internal::GetCapturedStderr(), + MatchesRegex( + ".+T.+Z \\[info\\] \\[logging_test.cc:10\\] Simple message\n")); +}; + +TEST(log, specifying_ctx) { + testing::internal::CaptureStderr(); + log(info, "MyCtx", "Message"); + EXPECT_THAT(testing::internal::GetCapturedStderr(), + MatchesRegex(".+T.+Z \\[info\\] \\[MyCtx\\] Message\n")); +}; + +TEST(log, formatting_parameters) { + testing::internal::CaptureStderr(); + log(info, "MyCtx", "String={} Int={}", "Hello", 42); + EXPECT_THAT( + testing::internal::GetCapturedStderr(), + MatchesRegex(".+T.+Z \\[info\\] \\[MyCtx\\] String=Hello Int=42\n")); +};