8000 Add tests for async registry pretty printer by jvolmer · Pull Request #21720 · arangodb/arangodb · GitHub
[go: up one dir, main page]

Skip to content

Add tests for async registry pretty printer #21720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add tests and fix small bugs in stacktrace printing
  • Loading branch information
jvolmer committed May 19, 2025
commit b7785c715150432bf27b10baeacf96397dd44236
1 change: 1 addition & 0 deletions arangod/AsyncRegistryServer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ target_link_libraries(arangoserver
arango_async_registry_stacktrace)

add_subdirectory(Stacktrace)
add_subdirectory(PrettyPrinter)
1 change: 1 addition & 0 deletions arangod/AsyncRegistryServer/PrettyPrinter/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add_subdirectory(src)
12 changes: 9 additions & 3 deletions arangod/AsyncRegistryServer/PrettyPrinter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,26 @@ This ensures that the pretty-printer is loaded to gdb. You can check if it is lo

### Run tests

Run all tests via
```
cmake --build --target async_registry_pretty_printer_test
```

#### Unit tests

Inside src-folder run unittests via
```
python3 -m unittest discover
```
or use cmake via `cmake --build --target async_registry_pretty_printer_python_test`.

#### Integration tests using gdb

```
cmake --build --preset my-edition --target gdb_pretty_printer
ctest --build --preset my-edition -R test_registry_pretty_printer
cmake --build --target async_registry_gdb_pretty_printer
ctest --build -R async_registry_gdb_pretty_printer_test
```
use `-V` option on `ctest` to see gdb output, e.g. printing
use `-V` option on `ctest` to see gdb debug output

## Pretty printing the REST call

Expand Down
2 changes: 2 additions & 0 deletions arangod/AsyncRegistryServer/PrettyPrinter/src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
add_custom_target(async_registry_pretty_printer_python_test ALL
COMMAND ${PYTHON_EXECUTABLE} -m unittest discover ${CMAKE_CURRENT_SOURCE_DIR})
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def branch_ascii(hierarchy: int, continuations: list) -> str:
ascii = [" " for x in range(2*hierarchy+2)] + [branching_symbol(hierarchy, continuations[-1] if len(continuations) > 0 else None)] + [" "]
for continuation in continuations:
if continuation < hierarchy:
ascii[2*continuation] = "│"
ascii[2*continuation+2] = "│"
return ''.join(ascii)

class Stacktrace(object):
Expand All @@ -26,7 +26,7 @@ def append(self, hierarchy: int, promise: Any) -> Optional[list[str, Any]]:
ascii = branch_ascii(hierarchy, self.stack)

# push current but not if already on stack
if len(self.stack) == 0 or hierarchy != self.stack[0]:
if len(self.stack) == 0 or hierarchy != self.stack[-1]:
self.stack.append(hierarchy)

self.lines.append((ascii, promise))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import unittest

from asyncregistry.stacktrace import Stacktrace

class StacktraceTest(unittest.TestCase):
def test_directly_gives_back_hierarchy_zero_entry(self):
stacktrace = Stacktrace()
self.assertEqual(stacktrace.append(0, 'entry'), [(' ┌ ', 'entry')])

def test_indents_consecutively_descending_hierarchie_levels(self):
stacktrace = Stacktrace()
self.assertEqual(stacktrace.append(3, 'hierarchy 3'), None)
self.assertEqual(stacktrace.append(2, 'hierarchy 2'), None)
self.assertEqual(stacktrace.append(1, 'hierarchy 1'), None)
self.assertEqual(stacktrace.append(0, 'hierarchy 0'),
[
(' ┌ ', 'hierarchy 3'),
(' ┌ ', 'hierarchy 2'),
(' ┌ ', 'hierarchy 1'),
(' ┌ ', 'hierarchy 0')
])

def test_combines_lines_of_consecutive_entries_of_the_same_hierarchy_level(self):
stacktrace = Stacktrace()
self.assertEqual(stacktrace.append(1, 'hierarchy 1 a'), None)
self.assertEqual(stacktrace.append(1, 'hierarchy 1 b'), None)
self.assertEqual(stacktrace.append(0, 'hierarchy 0'),
[
(' ┌ ', 'hierarchy 1 a'),
(' ├ ', 'hierarchy 1 b'),
(' ┌ ', 'hierarchy 0')
])

def test_continues_lines_of_non_finished_hierarchies(self):
stacktrace = Stacktrace()
self.assertEqual(stacktrace.append(1, 'hierarchy 1 a'), None)
self.assertEqual(stacktrace.append(2, 'hierarchy 2 a'), None)
self.assertEqual(stacktrace.append(3, 'hierarchy 3'), None)
self.assertEqual(stacktrace.append(2, 'hierarchy 2 b'), None)
self.assertEqual(stacktrace.append(1, 'hierarchy 1 b'), None)
self.assertEqual(stacktrace.append(0, 'hierarchy 0'),
[
(' ┌ ', 'hierarchy 1 a'),
(' │ ┌ ', 'hierarchy 2 a'),
(' │ │ ┌ ', 'hierarchy 3'),
(' │ ├ ', 'hierarchy 2 b'),
(' ├ ', 'hierarchy 1 b'),
(' ┌ ', 'hierarchy 0')
])

def test_works_also_for_non_consecutive_hierarchy_levels(self):
stacktrace = Stacktrace()
self.assertEqual(stacktrace.append(1, 'hierarchy 1'), None)
self.assertEqual(stacktrace.append(5, 'hierarchy 5'), None)
self.assertEqual(stacktrace.append(2, 'hierarchy 2'), None)
self.assertEqual(stacktrace.append(0, 'hierarchy 0'),
[
(' ┌ ', 'hierarchy 1'),
(' │ ┌ ', 'hierarchy 5'),
(' │ ┌ ', 'hierarchy 2'),
(' ┌ ', 'hierarchy 0')
])




if __name__ == '__main__':
unittest.main()
15 changes: 8 additions & 7 deletions tests/AsyncRegistryServer/PrettyPrinter/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
add_executable(gdb_pretty_printer
async_registry_test.cpp)
target_link_libraries(gdb_pretty_printer
add_executable(async_registry_gdb_pretty_printer
async_registry_gdb_pretty_printer_test.cpp)
target_link_libraries(async_registry_gdb_pretty_printer
PRIVATE
arango_async_registry
arango
)

enable_testing()

add_test(NAME test_registry_pretty_printer
add_test(NAME async_registry_gdb_pretty_printer_test
COMMAND gdb
$<TARGET_FILE:gdb_pretty_printer>
$<TARGET_FILE:async_registry_gdb_pretty_printer>
-ix ${PROJECT_SOURCE_DIR}/arangod/AsyncRegistryServer/PrettyPrinter/.gdbinit
-x ${CMAKE_CURRENT_SOURCE_DIR}/test.gdbscript
-batch

WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})

add_custom_target(async_registry_pretty_printer_test ALL COMMAND ${CMAKE_CTEST_COMMAND}
DEPENDS async_registry_gdb_pretty_printer)
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2014-2024 ArangoDB GmbH, Cologne, Germany
/// Copyright 2004-2014 triAGENS GmbH, Cologne, Germany
///
/// Licensed under the Business Source License 1.1 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://github.com/arangodb/arangodb/blob/devel/LICENSE
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
///
/// @author Julia Volmer
////////////////////////////////////////////////////////////////////////////////

#include "Async/Registry/registry_variable.h"
#include "Containers/Concurrent/thread.h"

#include <csignal>
#include <iostream>
#include <source_location>

using namespace arangodb::async_registry;

auto breakpoint() { raise(SIGINT); }

auto format(PromiseSnapshot const& snapshot) -> std::string {
return fmt::format("\"{}\" (\"{}\":{}), thread {}, {}",
snapshot.source_location.function_name,
snapshot.source_location.file_name,
snapshot.source_location.line, snapshot.thread.kernel_id,
arangodb::inspection::json(snapshot.state));
}

/**
This test creates a new registry and consecutively adds more promises to this
registry. For each new promise added there exists a breakpoint where the
corresponding gdb script will pause and compare the string representation of
the registry against the given expected variable.

We use here a completely new registry and not the global async registry to
add parts of the registry by hand (which would otherwise not easily be
possible) in order to test all possible scenarios.
*/
int main() {
[[maybe_unused]] auto finished = false;

// empty registry
auto test_registry = Registry{};
auto thread_registry = ThreadRegistry::make();
auto current_thread = arangodb::basics::ThreadId::current();
test_registry.add(thread_registry);

breakpoint();

auto expected = std::string("async registry");

breakpoint();

// add a promise
auto parent = thread_registry->add([&]() {
return Promise{{current_thread}, std::source_location::current()};
});
expected = fmt::format(
"async registry = {{\n"
"[thread {}] = \n"
" ┌ {}\n"
"─ thread {}}}",
current_thread.kernel_id, format(parent->data.snapshot()),
current_thread.kernel_id);

breakpoint();

// add a promise that depends on parent promise
auto* child = thread_registry->add([&]() {
return Promise{{parent->data.id()}, std::source_location::current()};
});
expected = fmt::format(
"async registry = {{\n"
"[thread {}] = \n"
" ┌ {}\n"
" ┌ {}\n"
"─ thread {}}}",
current_thread.kernel_id, format(child->data.snapshot()),
format(parent->data.snapshot()), current_thread.kernel_id);

breakpoint();

// add another promise that depends on parent promise
auto* second_child = thread_registry->add([&]() {
return Promise{{parent->data.id()}, std::source_location::current()};
});
expected = fmt::format(
"async registry = {{\n"
"[thread {}] = \n"
" ┌ {}\n"
" ├ {}\n"
" ┌ {}\n"
"─ thread {}}}",
current_thread.kernel_id, format(child->data.snapshot()),
format(second_child->data.snapshot()), format(parent->data.snapshot()),
current_thread.kernel_id);

breakpoint();

// add a child to a child promise
auto* child_of_child = thread_registry->add([&]() {
return Promise{{child->data.id()}, std::source_location::current()};
});
expected = fmt::format(
"async registry = {{\n"
"[thread {}] = \n"
" ┌ {}\n"
" ┌ {}\n"
" ├ {}\n"
" ┌ {}\n"
"─ thread {}}}",
current_thread.kernel_id, format(child_of_child->data.snapshot()),
format(child->data.snapshot()), format(second_child->data.snapshot()),
format(parent->data.snapshot()), current_thread.kernel_id);

breakpoint();

// add a child to the second child promise
auto* child_of_second_child = thread_registry->add([&]() {
return Promise{{second_child->data.id()}, std::source_location::current()};
});
expected = fmt::format(
"async registry = {{\n"
"[thread {}] = \n"
" ┌ {}\n"
" ┌ {}\n"
" │ ┌ {}\n"
" ├ {}\n"
" ┌ {}\n"
"─ thread {}}}",
current_thread.kernel_id, format(child_of_child->data.snapshot()),
format(child->data.snapshot()),
format(child_of_second_child->data.snapshot()),
format(second_child->data.snapshot()), format(parent->data.snapshot()),
current_thread.kernel_id);

breakpoint();

// add a completely unrelated promise
auto* second_parent = thread_registry->add([&]() {
return Promise{{arangodb::basics::ThreadId::current()},
std::source_location::current()};
});
expected = fmt::format(
"async registry = {{\n"
"[thread {}] = \n"
" ┌ {}\n"
"─ thread {}, \n"
"[thread {}] = \n"
" ┌ {}\n"
" ┌ {}\n"
" │ ┌ {}\n"
" ├ {}\n"
" ┌ {}\n"
"─ thread {}}}",
current_thread.kernel_id, format(second_parent->data.snapshot()),
current_thread.kernel_id, current_thread.kernel_id,
format(child_of_child->data.snapshot()), format(child->data.snapshot()),
format(child_of_second_child->data.snapshot()),
format(second_child->data.snapshot()), format(parent->data.snapshot()),
current_thread.kernel_id);

breakpoint();

auto second_thread_registry = ThreadRegistry::make();
auto other_thread = arangodb::basics::ThreadId{};
test_registry.add(second_thread_registry);

// add a new promise on another thread
auto* parent_on_other_thread = second_thread_registry->add([&]() {
return Promise{{other_thread}, std::source_location::current()};
});
expected = fmt::format(
"async registry = {{\n"
"[thread {}] = \n"
" ┌ {}\n"
"─ thread {}, \n"
"[thread {}] = \n"
" ┌ {}\n"
" ┌ {}\n"
" │ ┌ {}\n"
" ├ {}\n"
" ┌ {}\n"
"─ thread {}, \n"
"[thread {}] = \n"
" ┌ {}\n"
"─ thread {}}}",
current_thread.kernel_id, format(second_parent->data.snapshot()),
current_thread.kernel_id, current_thread.kernel_id,
format(child_of_child->data.snapshot()), format(child->data.snapshot()),
format(child_of_second_child->data.snapshot()),
format(second_child->data.snapshot()), format(parent->data.snapshot()),
current_thread.kernel_id, other_thread.kernel_id,
format(parent_on_other_thread->data.snapshot()), other_thread.kernel_id);

breakpoint();

finished = true;
breakpoint();

return 0;
}
Loading
0