Skip to content

Commit c1f7cb4

Browse files
committed
Capture and inspect all logs in Catch2 tests
This fixes a long-standing race condition that existed with test_utils::log_capture, which is has now been removed. Tests now unconditionally capturing logs in the global thread-safe test_utils::log_test_capture. This allows the test code to query captured messages, and will echo the captured logs through a global Catch2 hook if the test fails. By default, this mechanism will fail any test that logs a warning or error, but it provides functions to relax that behavior either explicitly through test_utils::allow_max_log_level or implicitly through runtime_fixture / device_queue_fixture which permit system-dependent warnings to appear. The "log.h" interface has also been simplified somewhat.
1 parent 7e99eed commit c1f7cb4

21 files changed

+567
-359
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Versioning](http://semver.org/spec/v2.0.0.html).
2828
- In edge cases, command graph generation would fail to generate await-push commands when re-distributing reduction results (#223)
2929
- Command graph generation was missing an anti-dependency between push-commands of partial reduction results and the final reduction command (#223)
3030
- Don't create multiple smaller push-commands instead of a single large one in some rare situations (#229)
31+
- Unit tests that inspect logs contained a race that would cause spurious failures (??)
3132

3233
## [0.4.1] - 2023-09-08
3334

include/config.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
#include <cstddef>
44
#include <optional>
5-
#include <string_view>
6-
#include <vector>
5+
6+
#include "log.h"
77

88
namespace celerity {
99
namespace detail {
@@ -27,6 +27,8 @@ namespace detail {
2727
*/
2828
config(int* argc, char** argv[]);
2929

30+
log_level get_log_level() const { return m_log_lvl; }
31+
3032
const host_config& get_host_config() const { return m_host_cfg; }
3133

3234
/**
@@ -50,6 +52,7 @@ namespace detail {
5052
std::optional<int> get_horizon_max_parallelism() const { return m_horizon_max_parallelism; }
5153

5254
private:
55+
log_level m_log_lvl;
5356
host_config m_host_cfg;
5457
std::optional<device_config> m_device_cfg;
5558
std::optional<bool> m_enable_device_profiling;

include/log.h

Lines changed: 35 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
#pragma once
22

33
#include <string>
4-
#include <string_view>
5-
#include <unordered_map>
6-
#include <utility>
7-
#include <variant>
84

95
#include <spdlog/spdlog.h>
106

11-
#include "print_utils.h"
7+
#include "print_utils.h" // any translation unit that needs logging probably also wants pretty-printing
8+
#include "utils.h"
129

13-
#define CELERITY_LOG_SET_SCOPED_CTX(ctx) CELERITY_DETAIL_LOG_SET_SCOPED_CTX(ctx)
1410

1511
#define CELERITY_LOG(level, ...) \
1612
(::spdlog::should_log(level) \
17-
? SPDLOG_LOGGER_CALL(::spdlog::default_logger_raw(), level, "{}{}", *::celerity::detail::active_log_ctx, ::fmt::format(__VA_ARGS__)) \
13+
? SPDLOG_LOGGER_CALL(::spdlog::default_logger_raw(), level, "{}{}", ::celerity::detail::active_log_ctx->repr, ::fmt::format(__VA_ARGS__)) \
1814
: (void)0)
1915

2016
// TODO Add a macro similar to SPDLOG_ACTIVE_LEVEL, configurable through CMake
@@ -25,68 +21,44 @@
2521
#define CELERITY_ERROR(...) CELERITY_LOG(::celerity::detail::log_level::err, __VA_ARGS__)
2622
#define CELERITY_CRITICAL(...) CELERITY_LOG(::celerity::detail::log_level::critical, __VA_ARGS__)
2723

28-
namespace celerity {
29-
namespace detail {
24+
namespace celerity::detail {
3025

31-
using log_level = spdlog::level::level_enum;
26+
using log_level = spdlog::level::level_enum;
3227

33-
template <typename... Es>
34-
struct log_map {
35-
const std::tuple<Es...>& entries;
36-
log_map(const std::tuple<Es...>& entries) : entries(entries) {}
37-
};
38-
39-
struct log_context {
40-
std::string value;
41-
log_context() = default;
42-
template <typename... Es>
43-
explicit log_context(const std::tuple<Es...>& entries) {
44-
static_assert(sizeof...(Es) % 2 == 0, "log_context requires key/value pairs");
45-
value = fmt::format("[{}] ", log_map{entries});
46-
}
47-
};
48-
49-
inline const std::string null_log_ctx;
50-
inline thread_local const std::string* active_log_ctx = &null_log_ctx;
28+
struct log_context {
29+
std::string repr;
5130

52-
struct log_ctx_setter {
53-
log_ctx_setter(log_context& ctx) { celerity::detail::active_log_ctx = &ctx.value; }
54-
~log_ctx_setter() { celerity::detail::active_log_ctx = &celerity::detail::null_log_ctx; }
55-
};
31+
log_context() = default;
5632

57-
#define CELERITY_DETAIL_LOG_SET_SCOPED_CTX(ctx) \
58-
log_ctx_setter _set_log_ctx_##__COUNTER__ { ctx }
59-
60-
template <typename Tuple, typename Callback>
61-
constexpr void tuple_for_each_pair_impl(const Tuple&, Callback&&, std::index_sequence<>) {}
62-
63-
template <typename Tuple, size_t I1, size_t I2, size_t... Is, typename Callback>
64-
constexpr void tuple_for_each_pair_impl(const Tuple& tuple, const Callback& cb, std::index_sequence<I1, I2, Is...>) {
65-
cb(std::get<I1>(tuple), std::get<I2>(tuple));
66-
tuple_for_each_pair_impl(tuple, cb, std::index_sequence<Is...>{});
67-
}
68-
69-
template <typename Tuple, typename Callback>
70-
constexpr void tuple_for_each_pair(const Tuple& tuple, const Callback& cb) {
71-
static_assert(std::tuple_size_v<Tuple> % 2 == 0, "an even number of entries is required");
72-
tuple_for_each_pair_impl(tuple, cb, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
33+
template <typename... Es>
34+
explicit log_context(const std::tuple<Es...>& entries) {
35+
static_assert(sizeof...(Es) % 2 == 0, "log_context requires key/value pairs");
36+
if constexpr(sizeof...(Es) > 0) {
37+
repr += "[";
38+
int i = 0;
39+
celerity::detail::utils::tuple_for_each_pair(entries, [&](const auto& a, const auto& b) {
40+
if(i++ > 0) { repr += ", "; }
41+
fmt::format_to(std::back_inserter(repr), "{}={}", a, b);
42+
});
43+
repr += "] ";
44+
}
7345
}
46+
};
7447

75-
} // namespace detail
76-
} // namespace celerity
48+
inline const log_context null_log_ctx;
49+
inline thread_local const log_context* active_log_ctx = &null_log_ctx;
7750

78-
template <typename... Es>
79-
struct fmt::formatter<celerity::detail::log_map<Es...>> {
80-
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
51+
class set_log_context_guard {
52+
public:
53+
set_log_context_guard(const log_context& ctx) : m_ctx_before(celerity::detail::active_log_ctx) { celerity::detail::active_log_ctx = &ctx; }
54+
set_log_context_guard(const set_log_context_guard&) = delete;
55+
set_log_context_guard(set_log_context_guard&&) = delete;
56+
set_log_context_guard& operator=(const set_log_context_guard&) = delete;
57+
set_log_context_guard& operator=(set_log_context_guard&&) = delete;
58+
~set_log_context_guard() { celerity::detail::active_log_ctx = m_ctx_before; }
8159

82-
template <typename FormatContext>
83-
auto format(const celerity::detail::log_map<Es...>& map, FormatContext& ctx) {
84-
auto&& out = ctx.out();
85-
int i = 0;
86-
tuple_for_each_pair(map.entries, [&i, &out](auto& a, auto& b) {
87-
if(i++ > 0) { fmt::format_to(out, ", "); }
88-
fmt::format_to(out, "{}={}", a, b);
89-
});
90-
return out;
91-
}
60+
private:
61+
const log_context* m_ctx_before;
9262
};
63+
64+
} // namespace celerity::detail

include/utils.h

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,36 +60,55 @@ struct pair_hash {
6060
}
6161
};
6262

63-
namespace utils_detail {
64-
65-
template <typename... Without, typename... ToKeep, typename T, typename... Ts>
66-
static auto tuple_without_impl(const std::tuple<ToKeep...>& to_keep, const std::tuple<T, Ts...>& to_check) {
67-
if constexpr((std::is_same_v<T, Without> || ...)) {
68-
if constexpr(sizeof...(Ts) == 0) {
69-
return to_keep;
70-
} else {
71-
return tuple_without_impl<Without...>(to_keep, std::tuple{std::get<Ts>(to_check)...});
72-
}
63+
} // namespace celerity::detail::utils
64+
65+
namespace celerity::detail::utils_detail {
66+
67+
template <typename... Without, typename... ToKeep, typename T, typename... Ts>
68+
constexpr auto tuple_without_impl(const std::tuple<ToKeep...>& to_keep, const std::tuple<T, Ts...>& to_check) {
69+
if constexpr((std::is_same_v<T, Without> || ...)) {
70+
if constexpr(sizeof...(Ts) == 0) {
71+
return to_keep;
7372
} else {
74-
if constexpr(sizeof...(Ts) == 0) {
75-
return std::tuple_cat(to_keep, to_check);
76-
} else {
77-
return tuple_without_impl<Without...>(std::tuple_cat(to_keep, std::tuple{std::get<T>(to_check)}), std::tuple{std::get<Ts>(to_check)...});
78-
}
73+
return tuple_without_impl<Without...>(to_keep, std::tuple{std::get<Ts>(to_check)...});
74+
}
75+
} else {
76+
if constexpr(sizeof...(Ts) == 0) {
77+
return std::tuple_cat(to_keep, to_check);
78+
} else {
79+
return tuple_without_impl<Without...>(std::tuple_cat(to_keep, std::tuple{std::get<T>(to_check)}), std::tuple{std::get<Ts>(to_check)...});
7980
}
8081
}
82+
}
8183

82-
} // namespace utils_detail
84+
template <typename Tuple, typename Callback>
85+
constexpr void tuple_for_each_pair_impl(const Tuple&, Callback&&, std::index_sequence<>) {}
86+
87+
template <typename Tuple, size_t I1, size_t I2, size_t... Is, typename Callback>
88+
constexpr void tuple_for_each_pair_impl(const Tuple& tuple, const Callback& cb, std::index_sequence<I1, I2, Is...>) {
89+
cb(std::get<I1>(tuple), std::get<I2>(tuple));
90+
tuple_for_each_pair_impl(tuple, cb, std::index_sequence<Is...>{});
91+
}
92+
93+
} // namespace celerity::detail::utils_detail
94+
95+
namespace celerity::detail::utils {
8396

8497
template <typename... Without, typename... Ts>
85-
static auto tuple_without(const std::tuple<Ts...>& tuple) {
98+
constexpr auto tuple_without(const std::tuple<Ts...>& tuple) {
8699
if constexpr(sizeof...(Ts) > 0) {
87100
return utils_detail::tuple_without_impl<Without...>({}, tuple);
88101
} else {
89102
return tuple;
90103
}
91104
}
92105

106+
template <typename Tuple, typename Callback>
107+
constexpr void tuple_for_each_pair(const Tuple& tuple, const Callback& cb) {
108+
static_assert(std::tuple_size_v<Tuple> % 2 == 0, "an even number of entries is required");
109+
utils_detail::tuple_for_each_pair_impl(tuple, cb, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
110+
}
111+
93112
/// Fiddles out the base name of a (possibly templated) struct or class from a full (possibly mangled) type name.
94113
/// The input parameter should be `typeid(Struct*)`, i.e. a _pointer_ to the desired struct type.
95114
std::string get_simplified_type_name_from_pointer(const std::type_info& pointer_type_info);

src/config.cc

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,10 @@ namespace detail {
170170
// ------------------------------- CELERITY_LOG_LEVEL ---------------------------------
171171

172172
#if defined(CELERITY_DETAIL_ENABLE_DEBUG)
173-
const auto log_lvl = parsed_and_validated_envs.get_or(env_log_level, log_level::debug);
173+
m_log_lvl = parsed_and_validated_envs.get_or(env_log_level, log_level::debug);
174174
#else
175-
const auto log_lvl = parsed_and_validated_envs.get_or(env_log_level, log_level::info);
175+
m_log_lvl = parsed_and_validated_envs.get_or(env_log_level, log_level::info);
176176
#endif
177-
// Set both the global log level and the default sink level so that the console logger adheres to CELERITY_LOG_LEVEL even if we temporarily
178-
// override the global level in test_utils::log_capture.
179-
// TODO do not modify global state in the constructor, but factor the LOG_LEVEL part out of detail::config entirely.
180-
spdlog::set_level(log_lvl);
181-
for(auto& sink : spdlog::default_logger_raw()->sinks()) {
182-
sink->set_level(log_lvl);
183-
}
184177

185178
// --------------------------------- CELERITY_DEVICES ---------------------------------
186179

src/runtime.cc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,10 @@ namespace detail {
121121
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
122122
m_local_nid = world_rank;
123123

124-
spdlog::set_pattern(fmt::format("[%Y-%m-%d %H:%M:%S.%e] [{:0{}}] [%^%l%$] %v", world_rank, int(ceil(log10(world_size)))));
124+
if(!m_test_mode) { // do not touch logger settings in tests, where the full (trace) logs are captured
125+
spdlog::set_level(m_cfg->get_log_level());
126+
spdlog::set_pattern(fmt::format("[%Y-%m-%d %H:%M:%S.%e] [{:0{}}] [%^%l%$] %v", world_rank, int(ceil(log10(world_size)))));
127+
}
125128

126129
#ifndef __APPLE__
127130
if(const uint32_t cores = affinity_cores_available(); cores < min_cores_needed) {

src/utils.cc

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,17 @@ void set_panic_solution(panic_solution solution) { g_panic_solution.store(soluti
7171

7272
[[noreturn]] void panic(const std::string& msg) {
7373
switch(g_panic_solution.load(std::memory_order_relaxed)) {
74-
case celerity::detail::utils::panic_solution::throw_logic_error: //
74+
case celerity::detail::utils::panic_solution::throw_logic_error: {
7575
throw std::logic_error(msg);
76+
}
7677
case celerity::detail::utils::panic_solution::log_and_abort:
77-
default:
78-
if(spdlog::should_log(spdlog::level::critical)) {
79-
CELERITY_CRITICAL("panic: {}", msg);
80-
} else {
81-
fmt::print(stderr, "celerity-runtime panic: {}\n", msg);
82-
}
78+
default: {
79+
// Print directly instead of logging: The abort message must not be hidden by log level setting, and in tests would be captured without the logging
80+
// infrastructure having a chance of dumping the logs due to the abort.
81+
fmt::print(stderr, "celerity-runtime panic: {}\n", msg);
8382
std::abort();
8483
}
84+
}
8585
}
8686

8787
void report_error(const error_policy policy, const std::string& msg) {

src/worker_job.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ namespace detail {
1919
// --------------------------------------------------------------------------------------------------------------------
2020

2121
void worker_job::update() {
22-
CELERITY_LOG_SET_SCOPED_CTX(m_lctx);
22+
set_log_context_guard lctx_guard(m_lctx);
23+
2324
assert(m_running && !m_done);
2425
const auto before = std::chrono::steady_clock::now();
2526
m_done = execute(m_pkg);
@@ -40,7 +41,8 @@ namespace detail {
4041
}
4142

4243
void worker_job::start() {
43-
CELERITY_LOG_SET_SCOPED_CTX(m_lctx);
44+
set_log_context_guard lctx_guard(m_lctx);
45+
4446
assert(!m_running);
4547
m_running = true;
4648

test/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ set(TEST_TARGETS
5050
device_selection_tests
5151
)
5252

53-
add_library(test_main test_main.cc grid_test_utils.cc)
54-
set_test_target_parameters(test_main test_main.cc grid_test_utils.cc)
53+
add_library(test_main test_main.cc test_utils.cc grid_test_utils.cc)
54+
set_test_target_parameters(test_main test_main.cc test_utils.cc grid_test_utils.cc)
5555

5656
set(TEST_OBJ_LIST "")
5757
foreach(TEST_TARGET ${TEST_TARGETS})

0 commit comments

Comments
 (0)