diff --git a/.github/workflows/unix.yml b/.github/workflows/unix.yml index 412a20a..4e3cbf2 100644 --- a/.github/workflows/unix.yml +++ b/.github/workflows/unix.yml @@ -10,7 +10,7 @@ jobs: formatting: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install clang-format env: @@ -82,7 +82,7 @@ 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' @@ -135,3 +135,35 @@ jobs: - name: Run Tests Valgrind if: matrix.config.mode == 'Debug' && matrix.config.cc == 'gcc-12' run: valgrind --error-exitcode=1 --show-reachable=yes --leak-check=full ./build/utl-test + + doc: + runs-on: ubuntu-latest + 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 ninja ⚙️ ⬇️ + uses: seanmiddleditch/gha-setup-ninja@master + + - name: Invoke CMake to install deps ⚙️ + run: cmake -G Ninja -S . -B build + + - name: Generate HTML documentation 🏗️ + run: deps/docs/build_docs.sh + + - name: Deploy documentation onto GitHub Pages 🚀 + if: github.ref == 'refs/heads/master' + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: public/ 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/.gitignore b/.gitignore index 1a4e4ca..17e1cad 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,11 @@ *.sublime-* /.idea /deps +/docs/conf.py +/docs/Doxyfile +/docs/html +/docs/xml +/public /.vscode +_build/ .clang-tidy diff --git a/.pkg b/.pkg index a49b840..2c2de16 100644 --- a/.pkg +++ b/.pkg @@ -1,3 +1,7 @@ +[docs] + url=git@github.com:motis-project/docs.git + branch=main + commit=75dc89a53e9c2d78574fc0ffda698e69f1682ed2 [googletest] url=git@github.com:motis-project/googletest.git branch=master @@ -5,7 +9,7 @@ [fmt] url=git@github.com:motis-project/fmt.git branch=master - commit=edb385ac526c24bc917ec4a41bb0edb28f0ca59e + commit=dc10f83be70ac2873d5f8d1ce317596f1fd318a2 [cista] url=git@github.com:felixguendling/cista.git branch=master diff --git a/README.md b/README.md index 2c243be..8bc328a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -![Linux Build](https://github.com/motis-project/utl/workflows/Linux%20Build/badge.svg) +![Unix Build](https://github.com/motis-project/utl/workflows/Unix%20Build/badge.svg) ![Windows Build](https://github.com/motis-project/utl/workflows/Windows%20Build/badge.svg) - This repository contains universally useful C++ utilities. + +Documentation: https://motis-project.github.io/utl/ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9216e04 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,72 @@ +# 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. + +:::{toctree} +:maxdepth: 2 +:caption: Contents: +::: + +## Logging +The simplest way to produce log lines is to use the `utl:log()` function, +or the wrapping functions for the various log levels, +`utl::log_debug()`, `utl::log_info()` and `utl::log_error()`: +```c++ +#include "utl/logging.h" + +utl::log_info("MyCtx", "Simple message"); +``` + +The first parameter is the **context**, that provides an information of the origin of the log line inside MOTIS code. + +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 +You can insert variables in the message by using `{}` and passing them as extra arguments +(formatting is performed by the [fmt](https://fmt.dev>) library): +```c++ +utl::log_info("MyCtx", "String={} Int={}", "Hello", 42); +``` + +You can specify **metadata** using `.attrs()`: +```c++ +utl::log_info("MyCtx", "Message").attrs({{"key1", "value1"}, {"key2", "value2"}}); +``` + +### API details +:::{doxygenstruct} utl::log +:no-link: +:members: +::: + +:::{doxygenstruct} utl::log_debug +:no-link: +:members: +::: + +:::{doxygenstruct} utl::log_info +:no-link: +:members: +::: + +:::{doxygenstruct} utl::log_error +:no-link: +:members: +::: + +:::{note} +Those logging function are an exception to the rule that, in MOTIS, +we use [Aggregate Initialization](https://en.cppreference.com/w/cpp/language/aggregate_initialization) wherever possible, +but here we do not want to use `utl::log_info{...}`. +::: diff --git a/include/utl/logging.h b/include/utl/logging.h index 8359454..a189d9e 100644 --- a/include/utl/logging.h +++ b/include/utl/logging.h @@ -1,70 +1,123 @@ #pragma once -#ifdef LOGGING_HEADER -#include LOGGING_HEADER -#else - #include -#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__) +namespace utl { -#define uLOG(lvl) \ - utl::log() << "[" << utl::log::str[lvl] << "]" \ - << "[" << utl::time() << "]" \ - << "[" << FILE_NAME << ":" << __LINE__ << "]" \ - << " " +/// Log level +enum class log_level { debug, info, error }; -namespace utl { +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 ""; +} -struct log { - log() = default; +extern log_level log_verbosity; - log(log const&) = delete; - log& operator=(log const&) = delete; +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 - log(log&&) = default; - log& operator=(log&&) = default; + std::stringstream ss; + ss << std::put_time(&tmp, "%FT%TZ"); + return ss.str(); +} - template - friend log&& operator<<(log&& l, T&& t) { - std::clog << std::forward(t); - return std::move(l); +/// Produce a new log line at the given `level`. +template +struct log { + log(const char* ctx, fmt::format_string fmt_str, Args&&... args, + std::source_location const& loc = std::source_location::current()) + : loc_{loc}, + ctx_{ctx}, + msg_{fmt::format(fmt_str, std::forward(args)...)} {} + + ~log() { + if (LogLevel >= log_verbosity) { +#if defined(_WIN32) + auto const base_file_name = strrchr(loc_.file_name(), '\\') + ? strrchr(loc_.file_name(), '\\') + 1 + : loc_.file_name(); +#else + // On MacOS, due to a bug with Clang 15, the wrong filename + // is retrieved (logging.h instead of the calling file): + // https://github.com/llvm/llvm-project/issues/56379 + auto const base_file_name = strrchr(loc_.file_name(), '/') + ? strrchr(loc_.file_name(), '/') + 1 + : loc_.file_name(); +#endif + fmt::print(std::clog, "{time} [{level}] [{file}:{line}] [{ctx}] {msg}\n", + fmt::arg("time", now()), fmt::arg("level", to_str(LogLevel)), + fmt::arg("file", base_file_name), + fmt::arg("line", loc_.line()), fmt::arg("ctx", ctx_), + fmt::arg("msg", msg_)); + } } - ~log() { std::clog << std::endl; } + /// Add key-values metadata + void attrs( + std::initializer_list >&& + attrs) { + attrs_ = std::move(attrs); + } - static constexpr const char* const str[]{"emrg", "alrt", "crit", "erro", - "warn", "note", "info", "debg"}; + log_level level_; + std::source_location loc_; + char const* ctx_; + std::string msg_; + std::initializer_list > attrs_; }; -enum log_level { emrg, alrt, crit, err, warn, notice, info, debug }; +/// Produce a new DEBUG log line +template +struct log_debug : public log { + using log::log; +}; -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; -} +/// Produce a new INFO log line +template +struct log_info : public log { + using log::log; +}; -inline std::string time() { - time_t now; - std::time(&now); - return time(now); -} +/// Produce a new ERROR log line +template +struct log_error : public log { + using log::log; +}; -} // namespace utl +// Template deduction guides, to help the compiler distinguish between +// the variadic template Args... and the next argument std::source_location +// which has a default value: -#endif +template +log_debug(const char* ctx, fmt::format_string, + Args&&... args) -> log_debug; + +template +log_info(const char* ctx, fmt::format_string, + Args&&... args) -> log_info; + +template +log_error(const char* ctx, fmt::format_string, + Args&&... args) -> log_error; + +} // namespace utl diff --git a/include/utl/parallel_for.h b/include/utl/parallel_for.h index 5d75804..9b3cd84 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(); + utl::log_info("parallel_for", "{} {}/{}", desc, idx, jobs.size()); } func(jobs[idx]); }, diff --git a/include/utl/sorted_diff.h b/include/utl/sorted_diff.h index 6dfef47..34f4bd6 100644 --- a/include/utl/sorted_diff.h +++ b/include/utl/sorted_diff.h @@ -15,8 +15,9 @@ constexpr std::string_view to_str(op const o) { std::unreachable(); } -template -void sorted_diff(It a, It const a_end, It b, It const b_end, Lt&& cmp, +template +void sorted_diff(It1 a, It1End const a_end, It2 b, It2End const b_end, Lt&& cmp, Eq&& deep_eq, Fn&& fn) { while (a != a_end || b != b_end) { if (a == a_end) { @@ -41,8 +42,9 @@ void sorted_diff(It a, It const a_end, It b, It const b_end, Lt&& cmp, } } -template -void sorted_diff(Collection const& a, Collection const& b, Lt&& cmp, +template +void sorted_diff(Collection1 const& a, Collection2 const& b, Lt&& cmp, Eq&& deep_eq, Fn&& fn) { sorted_diff(begin(a), end(a), begin(b), end(b), std::forward(cmp), std::forward(deep_eq), std::forward(fn)); diff --git a/include/utl/to_vec.h b/include/utl/to_vec.h index 7b0e2e8..ee4338c 100644 --- a/include/utl/to_vec.h +++ b/include/utl/to_vec.h @@ -7,12 +7,13 @@ namespace utl { template -inline void transform_to(Container&& c, Output& out, UnaryOperation&& op) { +inline Output& transform_to(Container&& c, Output& out, UnaryOperation&& op) { using std::begin; using std::end; out.reserve(static_cast(std::distance(begin(c), end(c)))); std::transform(begin(c), end(c), std::back_inserter(out), std::forward(op)); + return out; } template diff --git a/src/logging.cc b/src/logging.cc new file mode 100644 index 0000000..cb7b11a --- /dev/null +++ b/src/logging.cc @@ -0,0 +1,7 @@ +#include "utl/logging.h" + +namespace utl { + +log_level log_verbosity = log_level::debug; + +} // namespace utl \ No newline at end of file diff --git a/src/timer.cc b/src/timer.cc index 850348b..f268eaa 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"; + utl::log_info("scoped_timer", "[{}] 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)"; + utl::log_info("scoped_timer", "[{}] 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)"; + utl::log_info("scoped_timer", "[{}] {} ({}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"; + utl::log_info("scoped_timer", "[{}] 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)"; + utl::log_info("scoped_timer", "[{}] 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)"; + utl::log_info("scoped_timer", "[{}] {} ({}ms)", name_, message, t); } -} // namespace utl \ No newline at end of file +} // namespace utl diff --git a/test/init_from_test.cc b/test/init_from_test.cc index c19d112..652ddcc 100644 --- a/test/init_from_test.cc +++ b/test/init_from_test.cc @@ -14,6 +14,7 @@ struct b { int y_; }; +#ifndef _MSC_VER // This use case does not work with MSVC TEST(init_from, init_from) { struct test {}; @@ -55,3 +56,4 @@ TEST(init_from, init_from) { auto tgt1 = utl::init_from(src); EXPECT_EQ(tgt1->str_, src.str_.get()); }; +#endif diff --git a/test/logging_test.cc b/test/logging_test.cc new file mode 100644 index 0000000..a734c82 --- /dev/null +++ b/test/logging_test.cc @@ -0,0 +1,87 @@ +#include + +#include +#include + +#include "utl/logging.h" + +using ::testing::MatchesRegex; + +TEST(log, can_send_info_msg) { + testing::internal::CaptureStderr(); + utl::log_info("MyCtx", "Message"); + EXPECT_THAT( + testing::internal::GetCapturedStderr(), + MatchesRegex( + ".+T.+Z \\[info\\] \\[logging.+:..\\] \\[MyCtx\\] Message\n")); +} + +TEST(log, can_send_debug_msg) { + testing::internal::CaptureStderr(); + utl::log_debug("MyCtx", "Message"); + EXPECT_THAT( + testing::internal::GetCapturedStderr(), + MatchesRegex( + ".+T.+Z \\[debug\\] \\[logging.+:..\\] \\[MyCtx\\] Message\n")); +} + +TEST(log, can_send_error_msg) { + testing::internal::CaptureStderr(); + utl::log_error("MyCtx", "Message"); + EXPECT_THAT( + testing::internal::GetCapturedStderr(), + MatchesRegex( + ".+T.+Z \\[error\\] \\[logging.+:..\\] \\[MyCtx\\] Message\n")); +} + +TEST(log, can_format_extra_params) { + testing::internal::CaptureStderr(); + auto const value = 42; + utl::log_info("MyCtx", "String={} Int={}", "Hello", value); + EXPECT_THAT(testing::internal::GetCapturedStderr(), + MatchesRegex(".+T.+Z \\[info\\] \\[logging.+:..\\] \\[MyCtx\\] " + "String=Hello Int=42\n")); +} + +TEST(log, accept_string_view_as_extra_param) { + testing::internal::CaptureStderr(); + std::string_view str{"world"}; + utl::log_info("MyCtx", "Hello {}!", str); + EXPECT_THAT(testing::internal::GetCapturedStderr(), + MatchesRegex(".+T.+Z \\[info\\] \\[logging.+:..\\] \\[MyCtx\\] " + "Hello world!\n")); +} + +TEST(log, accept_string_view_as_extra_param_inline) { + testing::internal::CaptureStderr(); + std::string str{"world"}; + utl::log_info("MyCtx", "Hello {}!", std::string_view{str}); + EXPECT_THAT(testing::internal::GetCapturedStderr(), + MatchesRegex(".+T.+Z \\[info\\] \\[logging.+:..\\] \\[MyCtx\\] " + "Hello world!\n")); +} + +TEST(log, can_have_optional_attrs) { + testing::internal::CaptureStderr(); + utl::log_info("MyCtx", "Message").attrs({{"key", "value"}}); + EXPECT_THAT( + testing::internal::GetCapturedStderr(), + MatchesRegex( + ".+T.+Z \\[info\\] \\[logging.+:..\\] \\[MyCtx\\] Message\n")); +} + +struct dtor { + friend std::ostream& operator<<(std::ostream& out, dtor const& x) { + return out << x.x_; + } + ~dtor() { std::clog << "DESTROY\n"; } + int x_; +}; +TEST(log, temporary_streamed) { + testing::internal::CaptureStderr(); + auto const tmp = []() { return dtor{.x_ = 9999}; }; + utl::log_info("MyCtx", "{} {}", fmt::streamed(tmp()), fmt::streamed(tmp())); + EXPECT_THAT(testing::internal::GetCapturedStderr(), + MatchesRegex(".+T.+Z \\[info\\] \\[logging.+:..\\] \\[MyCtx\\] " + "9999 9999\nDESTROY\nDESTROY\n")); +}