Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ if(TILEDB_TESTS)
add_library(tiledb_Catch2WithMain STATIC
test/support/src/tdb_catch_main.cc)
target_link_libraries(tiledb_Catch2WithMain PUBLIC assert_header Catch2::Catch2)
add_compile_definitions("TILEDB_INTERCEPTS")
endif()

# -------------------------------------------------------
Expand Down
133 changes: 133 additions & 0 deletions tiledb/common/util/intercept.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* @file intercept.h
*
* @section LICENSE
*
* The MIT License
*
* @copyright Copyright (c) 2025 TileDB, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @section DESCRIPTION
*
* This file provides declarations for interception, which allows us
* to run arbitrary code internally at pre-defined "interception points".
* This can be used to verify that a test case causes a specific event
* to occur, or it can be used to pause and resume tasks so as
* to simulate particular patterns of concurrent execution, or it can
* be used to simulate exceptions being thrown from particular blocks
* of code, etc. etc.
*/

#ifndef TILEDB_TEST_INTERCEPTION_H
#define TILEDB_TEST_INTERCEPTION_H

#if defined(TILEDB_INTERCEPTS)

#include "tiledb/common/macros.h"

#include <functional>
#include <list>

namespace tiledb::intercept {

/**
* A set of actions to perform at a logical interception point.
*
* An `InterceptionPoint` may capture values from an `INTERCEPT` (see below)
* and runs an arbitrary set of callbacks over those values.
*
* Do not use this directly; instead use the macros `DECLARE_INTERCEPT`,
* `DEFINE_INTERCEPT`, and `INTERCEPT` to create functions which
* respectively create and invoke callbacks of `InterceptionPoint`.
*/
template <std::copyable... T>
class InterceptionPoint {
using Self = InterceptionPoint<T...>;

public:
class CallbackRegistration {
Self& intercept_;

using iter = std::list<std::function<void(T&&...)>>::const_iterator;
iter callback_node_;

public:
CallbackRegistration(InterceptionPoint& intercept, iter node)
: intercept_(intercept)
, callback_node_(node) {
}

~CallbackRegistration() {
intercept_.callbacks_.erase(callback_node_);
}

DISABLE_COPY_AND_COPY_ASSIGN(CallbackRegistration);
};

void event(T&&... args) {
for (auto& callback : callbacks_) {
callback(std::forward<T>(args)...);
}
}

CallbackRegistration and_also(std::function<void(T&&...)>&& callback) {
callbacks_.push_back(std::move(callback));
return CallbackRegistration(*this, std::prev(callbacks_.end()));
}

private:
friend class CallbackRegistration;

std::list<std::function<void(T&&...)>> callbacks_;
};

} // namespace tiledb::intercept

#define DECLARE_INTERCEPT(name, ...) \
extern tiledb::intercept::InterceptionPoint<__VA_ARGS__>& name()

#define DEFINE_INTERCEPT(name, ...) \
tiledb::intercept::InterceptionPoint<__VA_ARGS__>& name() { \
static tiledb::intercept::InterceptionPoint<__VA_ARGS__> impl; \
return impl; \
}

#define INTERCEPT(name, ...) \
do { \
([](auto... args) { \
name().event(std::forward<decltype(args)>(args)...); \
})(__VA_ARGS__); \
} while (0)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be not a macro?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The macro is useful for

#ifdef TILEDB_INTERCEPTS
#define INTERCEPT(name, ...) <etc>
#else
#define INTERCEPT(...)
#endif


#else // not defined(TILEDB_INTERCEPTS)

/**
* Similar to `assert`, expand to nothing if TILEDB_INTERCEPTS is not enabled,
* so that we can leave the intercepts in the code and have them optimized
* out for production builds.
*/
#define DECLARE_INTERCEPT(...)
#define DEFINE_INTERCEPT(...)
#define INTERCEPT(...)

#endif

#endif
7 changes: 7 additions & 0 deletions tiledb/common/util/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,10 @@ this_target_sources(
unit_view_combo.cc
)
conclude(unit_test)

commence(unit_test intercept)
this_target_sources(
unit_intercept.cc
unit_no_intercept.cc
)
conclude(unit_test)
163 changes: 163 additions & 0 deletions tiledb/common/util/test/unit_intercept.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* @file unit_intercept.cc
*
* @section LICENSE
*
* The MIT License
*
* @copyright Copyright (c) 2025 TileDB, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @section DESCRIPTION
*
* This file contains unit tests and simple examples for the `INTERCEPT`
* capability.
*/

#include "tiledb/common/util/intercept.h"

#include <test/support/tdb_catch.h>

#include <barrier>
#include <thread>

// global variable for demonstrating intercept perfect forwarding
int global = 0;

namespace intercept {

DECLARE_INTERCEPT(my_library_function_entry);
DEFINE_INTERCEPT(my_library_function_entry);

// NB: DECLARE_INTERCEPT is not strictly necessary if everything is in the same
// file. The DECLARE also need not be in a header, the linker will resolve it
// correctly if the DECLARE is written in the unit test file and the DEFINE
// in the source file.
DEFINE_INTERCEPT(my_library_function_exit, int, std::string_view, int);

} // namespace intercept

/**
* Silly library function for demonstrating intercepts.
*/
int my_library_function(std::string_view arg) {
INTERCEPT(intercept::my_library_function_entry);

const int local = global++;
INTERCEPT(intercept::my_library_function_exit, global, arg, local);

return local;
}

/**
* Demonstrates using intercepts to log aspects of an execution
* which we might want to make assertions about in a test.
*/
TEST_CASE("Intercept log", "[intercept]") {
std::map<std::string, std::vector<std::pair<int, int>>> values;

{
auto cb = intercept::my_library_function_exit().and_also(
[&values](int snapshot_global, std::string_view arg, int local) {
values[std::string(arg)].emplace_back(
std::make_pair(snapshot_global, local));
});

global = 0;
my_library_function("foo");
my_library_function("bar");
my_library_function("foo");

CHECK(values.size() == 2);
CHECK(values["foo"] == std::vector<std::pair<int, int>>{{1, 0}, {3, 2}});
CHECK(values["bar"] == std::vector<std::pair<int, int>>{{2, 1}});
}

// now that the callback is de-registered we shouldn't see anything
const decltype(values) snapshot = values;
my_library_function("bar");
CHECK(values == snapshot);
}

/**
* Demonstrates using intercepts to simulate errors occurring
* within a library function. This can be very useful if it is known
* that the throwing of the error is causing problems, but the error
* itself is difficult to reproduce.
*/
TEST_CASE("Intercept simulate error", "[intercept]") {
// nothing happens
my_library_function("foo");

// we can register a callback to make it throw
{
auto cb = intercept::my_library_function_entry().and_also(
[]() { throw std::logic_error("intercept"); });
CHECK_THROWS(
my_library_function("foo"),
Catch::Matchers::ContainsSubstring("intercept"));
}

// now the callback is de-registered, it should not throw again
my_library_function("foo");
}

/**
* Demonstrates using intercepts to synchronize multiple threads,
* producing a deterministic behavior.
*/
TEST_CASE("Intercept synchronize", "[intercept]") {
global = 0;

std::barrier sync(2);

auto cb = intercept::my_library_function_exit().and_also(
[&sync](int snapshot_global, std::string_view, int) {
if (snapshot_global == 2) {
// waits for the main thread
sync.arrive_and_wait();
// the main thread has arrived; wait for its signal to resume
sync.arrive_and_wait();
}
});

std::vector<int> tt_values;

std::thread tt([&tt_values]() {
tt_values.push_back(my_library_function("foo"));
tt_values.push_back(my_library_function("bar"));
tt_values.push_back(my_library_function("baz"));
tt_values.push_back(my_library_function("gub"));
});

sync.arrive_and_wait();

// the thread is waiting for a signal to continue,
// we can run arbitrary code while it does so
global = 100;

sync.arrive_and_wait();

tt.join();

// because we synchronized the two threads we should always
// see exactly the same values
CHECK(tt_values == std::vector<int>{0, 1, 100, 101});
}
69 changes: 69 additions & 0 deletions tiledb/common/util/test/unit_no_intercept.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @file unit_no_intercept.cc
*
* @section LICENSE
*
* The MIT License
*
* @copyright Copyright (c) 2025 TileDB, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @section DESCRIPTION
*
* This file contains a simple test which validates that intercepts
* are no-ops when `TILEDB_INTERCEPTS` is not defined.
*/

#undef TILEDB_INTERCEPTS
#include "tiledb/common/util/intercept.h"

#include <test/support/tdb_catch.h>

namespace no_intercept {

DECLARE_INTERCEPT(not_my_library_function_entry, int);
DEFINE_INTERCEPT(not_my_library_function_entry, int);

// declare another symbol with a different type,
// this will not compile if the intercepts are not removed by the preprocessor
int not_my_library_function_entry(void) {
return 1;
}

DEFINE_INTERCEPT(test_case_body, int);

} // namespace no_intercept

TEST_CASE("Intercept undef declare and define", "[intercept]") {
REQUIRE(no_intercept::not_my_library_function_entry() == 1);
}

TEST_CASE("Intercept undef inline", "[intercept]") {
// An intercept with side effects is a bad idea,
// but this does illustrate that the preprocessor removes
// all of the intercept arguments
int a = 0;
INTERCEPT(test_case_body, a++);
REQUIRE(a == 0);

// and for good measure another symbol with the same name
volatile int test_case_body = 1;
REQUIRE(test_case_body == 1);
}
Loading