From dcf53ca205cdcb21fe4df04027385db0ff076458 Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Wed, 31 Jul 2024 20:47:51 +0100 Subject: [PATCH 01/11] XPath, descendants, and a splash of Sentry --- CMakeLists.txt | 18 +- README.md | 28 +++- rapidxml.hpp | 11 +- rapidxml_generator.hpp | 80 +++++++++ rapidxml_iterators.hpp | 148 ++++++++++++++++ rapidxml_predicates.hpp | 362 ++++++++++++++++++++++++++++++++++++++++ test/iterators.cpp | 48 ++++++ test/main.cc | 145 ++++++++++++++++ test/xpath.cpp | 181 ++++++++++++++++++++ 9 files changed, 1011 insertions(+), 10 deletions(-) create mode 100644 rapidxml_generator.hpp create mode 100644 rapidxml_predicates.hpp create mode 100644 test/main.cc create mode 100644 test/xpath.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ad83cd..68fb421 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ project(rapidxml) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) option(RAPIDXML_PERF_TESTS "Enable (very slow) performance tests" OFF) +option(RAPIDXML_SENTRY "Use Sentry (for tests only)" ON) include(FetchContent) FetchContent_Declare( @@ -16,6 +17,11 @@ set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) +if (RAPIDXML_SENTRY) + set(SENTRY_BACKEND inproc) + add_subdirectory(sentry-native EXCLUDE_FROM_ALL) +endif(RAPIDXML_SENTRY) + enable_testing() add_executable(rapidxml-test test/parse-simple.cpp @@ -29,10 +35,18 @@ add_executable(rapidxml-test test/perf.cpp rapidxml_wrappers.hpp test/iterators.cpp + rapidxml_predicates.hpp + test/xpath.cpp + rapidxml_generator.hpp + test/main.cc ) -target_link_libraries(rapidxml-test - GTest::gtest_main +target_link_libraries(rapidxml-test PRIVATE + GTest::gtest ) +if(RAPIDXML_SENTRY) + target_link_libraries(rapidxml-test PRIVATE sentry) + target_compile_definitions(rapidxml-test PRIVATE DWD_GTEST_SENTRY=1) +endif() target_include_directories(rapidxml-test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} diff --git a/README.md b/README.md index 11dbb6d..6c64828 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,21 @@ It has breaking changes, the largest of which are: * There is no need for string termination, now, so the parse function never terminates, and that option has vanished. * Return values that were previously bare pointers are now a safe wrapped pointer which ordinarily will check/throw for nullptr. * append/prepend/insert_node now also have an append/prepend/insert_element shorthand, which will allow an XML namespace to be included if wanted. -* Parsing data can be done from a container as well as a NUL-terminated buffer. A NUL-terminated buffer is slightly faster still, and will be used if possible. +* Parsing data can be done from a container as well as a NUL-terminated buffer. A NUL-terminated buffer remains slightly faster, and will be used if possible (for example, if you pass ina std::basic_string, it'll call c_str() on it and do that). Not breaking, but kind of nice: * The parse buffer is now treated as const, and will never be mutated. This incurs a slight performance penalty for handling long text values that have an encoded entity late in the string. -* The iterators library is now included by default, and updated to handle most idiomatic modern C++ operations. +* The iterators library is now included by default, and updated to m_handle most idiomatic modern C++ operations. Internal changes: * There is no longer a internal::measure or internal::compare; these just use the std::char_traits functions as used by the string_views. * Reserialization (that is, using the rapidxml::print family on a tree that is mostly or entirely from parsing) is now much faster, and will optimize itself to use simple buffer copies where the data is unchanged from parsing. -* Alignment of the allocator uses C++11's alignof/std::align, and so sould be more portable. +* Alignment of the allocator uses C++11's alignof/std::align, and so should be more portable. + +New features: +* Instead of the `doc->allocate_node` / `node->append_node` dance, you can now `node->append_element(name, value)`, where `name` can be either a `string` (or `string_view`, etc) or a tuple like {xmlns, local_name}, which will set an xmlns attribute if needed. +* There's a xpathish thing going on in `rapidxml_predicates`, which lets you search for (or iterate through) elements using a trivial subset of XPath. +* You can get access to containerish things in rapidxml_iterators by methods on nodes/documents, as `node.children()`, `node.attributes()` and a new `node.descendants()`. ### Fun @@ -36,7 +41,18 @@ for (auto & child : node.children()) { } ``` -More in tests/iterators.cpp +More in [test/iterators.cpp](./test/iterators.cpp) + +Of course, in this case it might be simpler to: + +```c++ +auto xpath = rapidxml::xpath::parse("/potato"); +for (auto & child : xp->all(node)) { + scream_for(joy); +} +``` + +More of that in [test/xpath.cpp](./test/xpath.cpp) For those of us who lose track of the buffer sometimes, clone_node() now takes an optional second argument of "true" if you want to also clone the strings. Otherwise, nodes will use string_views which reference the original parsed buffer. @@ -47,7 +63,7 @@ std::basic_string_view. Typical usage passed in 0, NULL, or nullptr for unwa and earlier - use C++23 ideally, but you can pass in {} instead. This should probably be a std::optional> instead. -## Changes +## Changes to the original I needed a library for fast XMPP processing (reading, processing, and reserializing), and this mostly fit the bill. However, not entirely, so this version adds: @@ -61,6 +77,8 @@ The other thing this fork added was a file of simple tests, which I've recently The original makes reference to an expansive test suite, but this was not included in the open source release. I'll expand these tests as and when I need to. +The tests use a driver which can optionally use Sentry for performance/error tracking; to enable, use the CMake option RAPIDXML_SENTRY, and clone the [sentry-native](https://github.com/getsentry/sentry-native) repository into the root, and when running `rapidxml-test`, set SENTRY_DSN in the environment. None of the submodules are needed, but it'll need libcurl, so `sudo apt install libcurl4-openssl-dev`. + ## Pull Requests Erm. I didn't expect any, so never set up any of the infrastructure for them - this was really a fork-of-convenience for me. Not that they're unwelcome, of course, just entirely unexpected. diff --git a/rapidxml.hpp b/rapidxml.hpp index 5cc6805..1fcd54a 100644 --- a/rapidxml.hpp +++ b/rapidxml.hpp @@ -163,6 +163,7 @@ namespace rapidxml template class xml_attribute; template class xml_document; template class children; + template class descendants; template class attributes; //! Enumeration listing all node types produced by the parser. @@ -1052,6 +1053,10 @@ namespace rapidxml return rapidxml::children{*this}; } + rapidxml::descendants descendants() const { + return rapidxml::descendants{optional_ptr>{const_cast *>(this)}}; + } + rapidxml::attributes attributes() const { return rapidxml::attributes{*this}; } @@ -1095,7 +1100,7 @@ namespace rapidxml } for (xml_node *child = m_last_node; child; child = child->m_prev_sibling) { if ((name.empty() || child->name() == name) - && (!xmlns || child->xmlns() == xmlns)) { + && (xmlns.empty() || child->xmlns() == xmlns)) { return child; } } @@ -1112,7 +1117,7 @@ namespace rapidxml optional_ptr> previous_sibling(view_type const & name = {}, view_type const & asked_xmlns = {}) const { assert(this->m_parent); // Cannot query for siblings if node has no parent - if (name) + if (!name.empty()) { view_type xmlns = asked_xmlns; if (xmlns.empty() && !name.empty()) { @@ -1122,7 +1127,7 @@ namespace rapidxml } for (xml_node *sibling = m_prev_sibling; sibling; sibling = sibling->m_prev_sibling) if ((name.empty() || sibling->name() == name) - && (!xmlns || sibling->xmlns() == xmlns)) + && (xmlns.empty() || sibling->xmlns() == xmlns)) return sibling; return nullptr; } diff --git a/rapidxml_generator.hpp b/rapidxml_generator.hpp new file mode 100644 index 0000000..8279121 --- /dev/null +++ b/rapidxml_generator.hpp @@ -0,0 +1,80 @@ +// +// Created by dave on 29/07/2024. +// + +#ifndef RAPIDXML_RAPIDXML_GENERATOR_HPP +#define RAPIDXML_RAPIDXML_GENERATOR_HPP + +#include +#include + +namespace rapidxml { + template + class generator { + public: + using value_pointer = std::remove_reference::type *; + struct handle_type; + struct promise_type { + value_pointer value; + + std::suspend_always yield_value(T & v) { + value = &v; + return {}; + } + + std::suspend_never initial_suspend() { + return {}; + } + + std::suspend_always final_suspend() noexcept { + return {}; // Change this to std::suspend_always + } + + void return_void() {} + + void unhandled_exception() { + std::terminate(); + } + + generator get_return_object() { + return generator{handle_type{handle_type::from_promise(*this)}}; + } + }; + + struct handle_type : std::coroutine_handle { + explicit handle_type(std::coroutine_handle h) : std::coroutine_handle(h) {} + + T &operator*() { + return *(this->promise().value); + } + + void operator++() { + this->resume(); + } + + bool operator!=(std::default_sentinel_t) const { + return !this->done(); + } + }; + + explicit generator(handle_type h) : m_handle(h) {} + + ~generator() { + if (m_handle) + m_handle.destroy(); + } + + handle_type begin() { + return m_handle; + } + + std::default_sentinel_t end() { + return std::default_sentinel; + } + + private: + handle_type m_handle{}; + }; +} + +#endif //RAPIDXML_RAPIDXML_GENERATOR_HPP diff --git a/rapidxml_iterators.hpp b/rapidxml_iterators.hpp index 953aee9..e5a0ba3 100644 --- a/rapidxml_iterators.hpp +++ b/rapidxml_iterators.hpp @@ -101,6 +101,126 @@ class node_iterator }; + //! Iterator of child nodes of xml_node + template +class descendant_iterator + { + public: + using value_type = xml_node; + using reference = xml_node &; + using pointer = xml_node *; + using iterator_category = std::bidirectional_iterator_tag; + using difference_type = long; + + descendant_iterator() + : m_parent(), m_node() + { + } + + explicit descendant_iterator(xml_node::ptr node) + : m_parent(node), m_node(node->first_node()) + { + } + + descendant_iterator(descendant_iterator && other) noexcept : m_parent(other.m_parent), m_node(other.m_node) {} + descendant_iterator(descendant_iterator const & other) : m_parent(other.m_parent), m_node(other.m_node) {} + + reference operator *() const + { + return const_cast(*m_node); + } + + pointer operator->() const + { + return const_cast(m_node.get()); + } + + descendant_iterator& operator++() + { + if (m_node->first_node()) { + m_node = m_node->first_node(); + } else if (m_node->next_sibling()) { + m_node = m_node->next_sibling(); + } else { + // Run out of children, so move upward until we can find a sibling. + while (true) { + m_node = m_node->parent(); + if (m_node == m_parent) { + m_node = nullptr; + break; + } + if (m_node->next_sibling()) { + m_node = m_node->next_sibling(); + break; + } + } + } + return *this; + } + + descendant_iterator operator++(int) + { + node_iterator tmp = *this; + ++(*this); + return tmp; + } + + descendant_iterator& operator--() + { + if (!m_node->previous_sibling()) { + m_node = m_node->parent(); + if (m_node == m_parent) { + m_node = nullptr; + } + } else { + m_node = m_node->previous_sibling(); + while (m_node->last_node()) { + m_node = m_node->last_node(); + } + } + return *this; + } + + descendant_iterator operator--(int) + { + node_iterator tmp = *this; + --(*this); + return tmp; + } + + bool operator == (const descendant_iterator& rhs) const + { + return m_node == rhs.m_node; + } + + bool operator != (const descendant_iterator& rhs) const + { + return m_node != rhs.m_node; + } + + descendant_iterator & operator = (descendant_iterator && other) noexcept { + m_parent = other.m_parent; + m_node = other.m_node; + return *this; + } + + descendant_iterator & operator = (descendant_iterator const & other) { + m_parent = other.m_parent; + m_node = other.m_node; + return *this; + } + + bool valid() + { + return m_node.has_value(); + } + + private: + + optional_ptr> m_parent; + optional_ptr> m_node; + }; + //! Iterator of child attributes of xml_node template class attribute_iterator @@ -217,6 +337,34 @@ class node_iterator } }; + //! Container adaptor for child nodes + template + class descendants + { + xml_node & m_node; + public: + explicit descendants(xml_node & node) : m_node(node) {} + explicit descendants(optional_ptr> ptr) : m_node(ptr.value()) {} + descendants(descendants && other) noexcept : m_node(other.m_node) {} + descendants(descendants const & other) : m_node(other.m_node) {} + + using const_iterator = descendant_iterator; + using iterator = descendant_iterator; + + iterator begin() { + return iterator(&m_node); + } + iterator end() { + return {}; + } + const_iterator begin() const { + return const_iterator(&m_node); + } + const_iterator end() const { + return {}; + } + }; + //! Container adaptor for attributes template class attributes diff --git a/rapidxml_predicates.hpp b/rapidxml_predicates.hpp new file mode 100644 index 0000000..3da4b5d --- /dev/null +++ b/rapidxml_predicates.hpp @@ -0,0 +1,362 @@ +// +// Created by dave on 29/07/2024. +// + +#ifndef RAPIDXML_RAPIDXML_PREDICATES_HPP +#define RAPIDXML_RAPIDXML_PREDICATES_HPP + +#include +#include +#include "rapidxml_generator.hpp" +#include "rapidxml.hpp" + +namespace rapidxml { + template class xpath; + namespace internal { + template + class xpath_base; + + template + class name : public rapidxml::internal::xpath_base { + private: + std::basic_string m_name; + std::optional> m_xmlns; + public: + explicit name(std::basic_string_view n) + : xpath_base(), m_name(n) {} + + explicit name(std::basic_string const & xmlns, std::basic_string_view n) + : xpath_base(), m_name(n), m_xmlns(xmlns) {} + + bool do_match(const xml_node & t) override { + if (m_xmlns.has_value() && t.xmlns() != m_xmlns.value()) return false; + return (t.type() == node_element) && (t.name() == m_name || m_name == "*"); + } + }; + + template + class value : public rapidxml::internal::xpath_base { + private: + std::basic_string m_value; + public: + explicit value(std::basic_string_view v) + : xpath_base(), m_value(v) {} + + bool do_match(const xml_node & t) override { + return (t.type() == node_element) && (t.value() == m_value); + } + }; + + template + class xmlns : public rapidxml::internal::xpath_base { + private: + std::basic_string m_xmlns; + public: + explicit xmlns(std::basic_string_view v) + : xpath_base(), m_xmlns(v) {} + + bool do_match(const xml_node & t) override { + return (t.type() == node_element) && (t.xmlns() == m_xmlns); + } + }; + + template + class attr : public rapidxml::internal::xpath_base { + private: + std::basic_string m_name; + std::basic_string m_value; + std::optional> m_xmlns; + public: + explicit attr(std::basic_string_view n, std::basic_string_view v) + : xpath_base(), m_name(n), m_value(v) {} + + explicit attr(std::basic_string const & x, std::basic_string_view n, std::basic_string_view v) + : xpath_base(), m_name(n), m_value(v), m_xmlns(x) {} + + bool do_match(const xml_node & t) override { + if (t.type() != node_element) return false; + for (auto attr : t.attributes()) { + if (m_xmlns.has_value()) { + if (m_name == "*" || attr.local_name() != m_name) continue; + if (attr.xmlns() != m_xmlns.value()) continue; + } else { + if (m_name == "*" || attr.name() != m_name) continue; + } + return attr.value() == m_value; + } + return false; + } + }; + + template + class root : public rapidxml::internal::xpath_base { + public: + root() = default; + + generator &> do_gather(xml_node & t) override { + for (auto & x : t.children()) { + co_yield x; + } + } + + bool do_match(const xml_node & t) override { + return t.type() == node_document || t.type() == node_element; + } + }; + + template + class any : public rapidxml::internal::xpath_base { + public: + any() = default; + + generator &> do_gather(xml_node & t) override { + co_yield t; // self + for (auto & x : t.descendants()) { + co_yield x; + } + } + + bool do_match(const xml_node & t) override { + return t.type() == node_document || t.type() == node_element; + } + }; + + template + class xpath_base { + private: + std::list>> m_contexts; + public: + + xpath_base() = default; + + virtual ~xpath_base() = default; + + virtual generator &> do_gather(xml_node & t) { + co_yield t; + } + + generator &> gather(xml_node & t) { + for (auto & x : do_gather(t)) { + if (match(x)) co_yield x; + } + } + + virtual bool do_match(const xml_node & t) = 0; + + bool match(xml_node & t) { + if (!do_match(t)) { + return false; + } + for(auto & context : m_contexts) { + if (!context->first(t)) { + return false; + } + } + return true; + } + + void context(std::unique_ptr> && xp) { + m_contexts.emplace_back(std::move(xp)); + } + + auto & contexts() const { + return m_contexts; + } + }; + + std::map xmlns_empty = {}; + } + + template + class xpath : public internal::xpath_base { + private: + std::vector>> m_chain; + std::map const & m_xmlns; + + public: + bool do_match(const xml_node & t) override { + return false; + } + + auto const & chain() const { + return m_chain; + } + std::string const & prefix_lookup(std::basic_string_view const & prefix) const { + std::basic_string p{prefix}; + auto it = m_xmlns.find(p); + if (it != m_xmlns.end()) { + return (*it).second; + } + throw std::runtime_error("XPath contains unknown prefix"); + } + + static void parse_predicate(std::basic_string_view const &name, xpath &xp, bool inner) { + using xml_doc = xml_document; + if (name.starts_with('@')) { + std::basic_string text = "(text); + auto attr = doc.first_node()->first_attribute(); + auto colon = attr->name().find(':'); + if (colon != xml_attribute::view_type::npos) { + auto const & uri = xp.prefix_lookup(attr->name().substr(0, colon)); + xp.m_chain.push_back(std::make_unique>(uri, attr->local_name(), attr->value())); + } else { + xp.m_chain.push_back(std::make_unique>(star ? "*" : attr->name(), attr->value())); + } + } else if (name.starts_with("text()")) { + // text match + std::basic_string text = "(text); + auto attr = doc.first_node()->first_attribute(); + xp.m_chain.push_back(std::make_unique>(attr->value())); + } else if (name.starts_with("namespace-uri()")) { + // text match + std::basic_string text = "(text); + auto attr = doc.first_node()->first_attribute(); + xp.m_chain.push_back(std::make_unique>(attr->value())); + } else { + if (xp.m_chain.empty() && inner) { + xp.m_chain.push_back(std::make_unique>()); + } + auto colon = name.find(':'); + if (colon != std::basic_string_view::npos) { + auto const & uri = xp.prefix_lookup(name.substr(0, colon)); + xp.m_chain.push_back(std::make_unique>(uri, name.substr(colon + 1))); + } else { + xp.m_chain.push_back(std::make_unique>(name)); + } + } + } + + static bool parse_inner(std::map & xmlns, std::basic_string_view &view, xpath &xp, bool first=false, bool inner=false) { + if (view.starts_with("//")) { + xp.m_chain.push_back(std::make_unique>()); + view.remove_prefix(2); + } else if (view.starts_with('/')) { + xp.m_chain.push_back(std::make_unique>()); + view.remove_prefix(1); + } else if (first && !inner) { + xp.m_chain.push_back(std::make_unique>()); + } + for (typename std::basic_string_view::size_type i = 0; i != view.size(); ++i) { + switch (view[i]) { + case '/': + case ']': + if (i == 0) throw std::runtime_error("Empty name?"); + case '[': + if (i != 0) parse_predicate(view.substr(0, i), xp, inner); + } + switch (view[i]) { + case ']': + view.remove_prefix(i + 1); + if (!inner) throw std::runtime_error("Unexpected ] in input"); + return true; + case '[': + view.remove_prefix(i + 1); + xp.m_chain[xp.m_chain.size() - 1]->context(parse_cont(xmlns, view)); + return false; + case '/': + view.remove_prefix(i ); + return false; + } + } + if (!view.empty()) { + parse_predicate(view, xp, inner); + view.remove_prefix(view.length()); + } + return true; + } + + static std::unique_ptr> parse_cont(std::map & xmlns, std::basic_string_view &view) { + if (view.empty()) throw std::runtime_error("Context expression is empty"); + auto xp = std::make_unique>(xmlns); + if (!parse_inner(xmlns, view, *xp, true, true)) { + while (!view.empty()) { + if (parse_inner(xmlns, view, *xp, false, true)) break; + } + } + return xp; + } + + static std::unique_ptr> parse(std::map & xmlns, std::basic_string_view &view) { + if (view.empty()) throw std::runtime_error("XPath expression is empty"); + auto xp = std::make_unique>(xmlns); + if (!parse_inner(xmlns, view, *xp, true, false)) { + while (!view.empty()) { + if (parse_inner(xmlns, view, *xp, false, false)) break; + } + } + return xp; + } + static std::unique_ptr> parse(std::map & xmlns, std::basic_string_view const &view) { + std::basic_string_view sv(view); + return parse(xmlns, sv); + } + static std::unique_ptr> parse(std::map & xmlns, std::basic_string const &view) { + std::basic_string_view sv(view); + return parse(xmlns, sv); + } + static std::unique_ptr> parse(std::map & xmlns, const char * view) { + std::basic_string_view sv(view); + return parse(xmlns, sv); + } + static std::unique_ptr> parse(std::basic_string_view &sv) { + return parse(internal::xmlns_empty, sv); + } + static std::unique_ptr> parse(std::basic_string const &view) { + std::basic_string_view sv(view); + return parse(internal::xmlns_empty, sv); + } + static std::unique_ptr> parse(std::basic_string_view const &view) { + std::basic_string_view sv(view); + return parse(internal::xmlns_empty, sv); + } + static std::unique_ptr> parse(const char * view) { + std::basic_string_view sv(view); + return parse(internal::xmlns_empty, sv); + } + + xpath(std::map & xmlns) : m_xmlns(xmlns) {} + + rapidxml::generator &> all(xml_node & current, int depth = 0) { + if (depth >= m_chain.size()) throw std::logic_error("Depth exceeded"); + auto & xp = m_chain[depth]; + depth++; + for (auto & r : xp->gather(current)) { + if (depth >= m_chain.size()) { + co_yield r; + } else { + for (auto & t : all(r, depth)) { + co_yield t; + } + } + } + } + + xml_node::ptr first(xml_node & current) { + for (auto &r: all(current)) { + return &r; + } + return {}; + } + }; +} + +#endif //RAPIDXML_RAPIDXML_PREDICATES_HPP diff --git a/test/iterators.cpp b/test/iterators.cpp index f79817a..141054b 100644 --- a/test/iterators.cpp +++ b/test/iterators.cpp @@ -66,6 +66,54 @@ TEST(Predicates, Nodes) { EXPECT_EQ(match->name(), "two"); } +TEST(Predicates, AllNodes) { + std::string xml = ""; + rapidxml::xml_document<> doc; + doc.parse(xml); + auto it = rapidxml::descendant_iterator<>(doc.first_node()); + EXPECT_EQ(it->name(), "one"); + ++it; + EXPECT_EQ(it->name(), "two"); + ++it; + EXPECT_EQ(it->name(), "three"); + ++it; + EXPECT_EQ(it->name(), "four"); + ++it; + EXPECT_EQ(it->name(), "five"); + ++it; + EXPECT_EQ(it->name(), "six"); + ++it; + EXPECT_FALSE(it.valid()); +} + +TEST(Predicates, AllNodesRev) { + std::string xml = ""; + rapidxml::xml_document<> doc; + doc.parse(xml); + auto it = rapidxml::descendant_iterator<>(doc.first_node()); + EXPECT_EQ(it->name(), "one"); + ++it; + EXPECT_EQ(it->name(), "two"); + ++it; + EXPECT_EQ(it->name(), "three"); + ++it; + EXPECT_EQ(it->name(), "four"); + ++it; + EXPECT_EQ(it->name(), "five"); + ++it; + EXPECT_EQ(it->name(), "six"); + --it; + EXPECT_EQ(it->name(), "five"); + --it; + EXPECT_EQ(it->name(), "four"); + --it; + EXPECT_EQ(it->name(), "three"); + --it; + EXPECT_EQ(it->name(), "two"); + --it; + EXPECT_EQ(it->name(), "one"); +} + TEST(Predicates, Attributes) { std::string xml = R"()"; rapidxml::xml_document<> doc; diff --git a/test/main.cc b/test/main.cc new file mode 100644 index 0000000..e9fae06 --- /dev/null +++ b/test/main.cc @@ -0,0 +1,145 @@ +// +// Created by dave on 30/07/2024. +// + +#include "gtest/gtest.h" +#ifdef DWD_GTEST_SENTRY +#include + +class EventListener : public ::testing::TestEventListener { + sentry_transaction_context_t *tx_ctx = nullptr; + sentry_transaction_t *tx = nullptr; + sentry_span_t *main_span = nullptr; + sentry_span_t *suite_span = nullptr; + sentry_span_t *test_span = nullptr; + std::string const & m_progname; +public: + EventListener(std::string const & progname) : m_progname(progname) {} + ~EventListener() override = default; + + // Override this to define how to set up the environment. + void OnTestProgramStart(const ::testing::UnitTest & u) override { + sentry_options_t *options = sentry_options_new(); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_init(options); + } + void OnTestProgramEnd(const ::testing::UnitTest &) override { + sentry_shutdown(); + } + + void OnTestStart(::testing::TestInfo const & test_info) override { + const char * testName = test_info.name(); + std::string tname = test_info.test_suite_name(); + tname += "."; + tname += testName; + test_span = sentry_span_start_child( + suite_span, + "test", + tname.c_str() + ); + } + + // Override this to define how to tear down the environment. + void OnTestEnd(const ::testing::TestInfo & ti) override { + if (ti.result()->Failed()) { + sentry_span_set_status(test_span, sentry_span_status_t::SENTRY_SPAN_STATUS_INTERNAL_ERROR); + } + sentry_span_finish(test_span); // Mark the span as finished + } + + void OnTestIterationStart(const testing::UnitTest &unit_test, int iteration) override { + tx_ctx = sentry_transaction_context_new( + m_progname.c_str(), + "googletest" + ); + tx = sentry_transaction_start(tx_ctx, sentry_value_new_null()); + main_span = sentry_transaction_start_child( + tx, + "googletest", + m_progname.c_str() + ); + } + + void OnEnvironmentsSetUpStart(const testing::UnitTest &unit_test) override { + + } + + void OnEnvironmentsSetUpEnd(const testing::UnitTest &unit_test) override { + + } + + void OnTestSuiteStart(const testing::TestSuite &suite) override { + suite_span = sentry_span_start_child( + main_span, + "test.suite", + suite.name() + ); + TestEventListener::OnTestSuiteStart(suite); + } + + void OnTestCaseStart(const testing::TestCase &aCase) override { + TestEventListener::OnTestCaseStart(aCase); + } + + void OnTestDisabled(const testing::TestInfo &info) override { + TestEventListener::OnTestDisabled(info); + } + + void OnTestPartResult(const testing::TestPartResult &test_part_result) override { + sentry_set_span(test_span); + auto val = sentry_value_new_breadcrumb("test", test_part_result.message()); + sentry_add_breadcrumb(val); + if (test_part_result.failed()) { + auto ev = sentry_value_new_event(); + auto exc = sentry_value_new_exception("GoogleTest", test_part_result.message()); + sentry_value_set_stacktrace(exc, nullptr, 0); + sentry_event_add_exception(ev, exc); + sentry_capture_event(ev); + } + } + + void OnTestSuiteEnd(const testing::TestSuite &suite) override { + TestEventListener::OnTestSuiteEnd(suite); + if (suite.failed_test_count() > 0) { + sentry_span_set_status(suite_span, sentry_span_status_t::SENTRY_SPAN_STATUS_INTERNAL_ERROR); + } + sentry_span_finish(suite_span); // Mark the span as finished + } + + void OnTestCaseEnd(const testing::TestCase &aCase) override { + TestEventListener::OnTestCaseEnd(aCase); + } + + void OnEnvironmentsTearDownStart(const testing::UnitTest &unit_test) override { + + } + + void OnEnvironmentsTearDownEnd(const testing::UnitTest &unit_test) override { + + } + + void OnTestIterationEnd(const testing::UnitTest &unit_test, int iteration) override { + if (unit_test.failed_test_count() > 0) { + sentry_span_set_status(main_span, sentry_span_status_t::SENTRY_SPAN_STATUS_INTERNAL_ERROR); + sentry_transaction_set_status(tx, sentry_span_status_t::SENTRY_SPAN_STATUS_INTERNAL_ERROR); + } + sentry_span_finish(main_span); // Mark the span as finished + sentry_transaction_finish(tx); + } +}; +#endif + +int main(int argc, char ** argv) { + std::string progname(argv[0]); + auto slash = progname.find_last_of("/\\"); + if (slash != std::string::npos) { + progname = progname.substr(slash + 1); + } + ::testing::InitGoogleTest(&argc, argv); + auto & listeners = ::testing::UnitTest::GetInstance()->listeners(); +#ifdef DWD_GTEST_SENTRY + listeners.Append(new EventListener(progname)); +#endif + auto ret = RUN_ALL_TESTS(); + return ret; +} diff --git a/test/xpath.cpp b/test/xpath.cpp new file mode 100644 index 0000000..de58d91 --- /dev/null +++ b/test/xpath.cpp @@ -0,0 +1,181 @@ +// +// Created by dave on 29/07/2024. +// + +#include +#include "rapidxml_predicates.hpp" + +TEST(XPath, parse) { + std::string xpath_string = "//"; + std::string_view sv{xpath_string}; + auto xp = rapidxml::xpath<>::parse(sv); + EXPECT_EQ(sv.length(), 0); + EXPECT_NE(xp.get(), nullptr); + EXPECT_EQ(xp->chain().size(), 1); +} + +TEST(XPath, parse2) { + std::string xpath_string = "//child"; + std::string_view sv{xpath_string}; + auto xp = rapidxml::xpath<>::parse(sv); + EXPECT_EQ(sv.length(), 0); + EXPECT_NE(xp.get(), nullptr); + EXPECT_EQ(xp->chain().size(), 2); +} + +TEST(XPath, parse1) { + std::string xpath_string = "/child"; + std::string_view sv{xpath_string}; + auto xp = rapidxml::xpath<>::parse(sv); + EXPECT_EQ(sv.length(), 0); + EXPECT_NE(xp.get(), nullptr); + EXPECT_EQ(xp->chain().size(), 2); +} + +TEST(XPath, parse3) { + std::string xpath_string = "//child[another/element]/something"; + std::string_view sv{xpath_string}; + auto xp = rapidxml::xpath<>::parse(sv); + EXPECT_EQ(sv.length(), 0); + EXPECT_NE(xp.get(), nullptr); + EXPECT_EQ(xp->chain().size(), 4); + ASSERT_EQ(xp->chain()[1]->contexts().size(), 1); + EXPECT_EQ(xp->chain()[1]->contexts().begin()->get()->chain().size(), 4); +} + +TEST(XPath, parse4) { + std::string xpath_string = ""; + std::string_view sv{xpath_string}; + EXPECT_THROW( + rapidxml::xpath<>::parse(sv), + std::runtime_error + ); +} + + +TEST(XPath, parse_attr) { + std::string xpath_string = "//child[@foo='bar']/something"; + std::string_view sv{xpath_string}; + auto xp = rapidxml::xpath<>::parse(sv); + EXPECT_EQ(sv.length(), 0); + EXPECT_NE(xp.get(), nullptr); + EXPECT_EQ(xp->chain().size(), 4); + ASSERT_EQ(xp->chain()[1]->contexts().size(), 1); + EXPECT_EQ(xp->chain()[1]->contexts().begin()->get()->chain().size(), 1); +} + +TEST(XPath, parse_text) { + std::string xpath_string = "//child[text()='bar']/something"; + std::string_view sv{xpath_string}; + auto xp = rapidxml::xpath<>::parse(sv); + EXPECT_EQ(sv.length(), 0); + EXPECT_NE(xp.get(), nullptr); + EXPECT_EQ(xp->chain().size(), 4); + ASSERT_EQ(xp->chain()[1]->contexts().size(), 1); + EXPECT_EQ(xp->chain()[1]->contexts().begin()->get()->chain().size(), 1); +} + +TEST(XPathFirst, simple_all) { + rapidxml::xml_document<> doc; + doc.parse(""); + std::string xpath = "//"; + std::string_view sv{xpath}; + auto xp = rapidxml::xpath<>::parse(sv); + auto r = xp->first(doc); + ASSERT_TRUE(r); + EXPECT_EQ(r->type(), rapidxml::node_document); +} + +TEST(XPathFirst, simple_any) { + rapidxml::xml_document<> doc; + doc.parse(""); + std::string xpath = "//child"; + std::string_view sv{xpath}; + auto xp = rapidxml::xpath<>::parse(sv); + auto r = xp->first(doc); + ASSERT_TRUE(r); + EXPECT_EQ(r->name(), "child"); +} + +TEST(XPathFirst, simple_sub) { + rapidxml::xml_document<> doc; + doc.parse(""); + std::string xpath = "//[child]"; + std::string_view sv{xpath}; + auto xp = rapidxml::xpath<>::parse(sv); + auto r = xp->first(doc); + ASSERT_TRUE(r); + EXPECT_EQ(r->name(), "simple"); +} + +TEST(XPathFirst, simple_attr) { + rapidxml::xml_document<> doc; + doc.parse("foobar"); + std::string xpath = "//child[@attr='val2']"; + std::string_view sv{xpath}; + auto xp = rapidxml::xpath<>::parse(sv); + auto r = xp->first(doc); + ASSERT_TRUE(r); + EXPECT_EQ(r->name(), "child"); + EXPECT_EQ(r->value(), "bar"); +} + +TEST(XPathFirst, simple_text) { + rapidxml::xml_document<> doc; + doc.parse("foobar"); + auto xp = rapidxml::xpath<>::parse("//child[text()='bar']"); + auto r = xp->first(doc); + ASSERT_TRUE(r); + EXPECT_EQ(r->name(), "child"); + EXPECT_EQ(r->value(), "bar"); +} + +TEST(XPathNS, simple_text) { + rapidxml::xml_document<> doc; + doc.parse("foobar"); + auto xp = rapidxml::xpath<>::parse("//child[text()='bar']"); + auto r = xp->first(doc); + ASSERT_TRUE(r); + EXPECT_EQ(r->name(), "child"); + EXPECT_EQ(r->value(), "bar"); +} + +TEST(XPathNS, xmlns_text) { + rapidxml::xml_document<> doc; + doc.parse("foobar"); + std::map xmlns = { + {"x1", "p2"}, + {"x2", "p1"} + }; + auto xp = rapidxml::xpath<>::parse(xmlns,"//x1:child[text()='bar']"); + auto r = xp->first(doc); + ASSERT_TRUE(r); + EXPECT_EQ(r->name(), "child"); + EXPECT_EQ(r->value(), "bar"); +} + +TEST(XPathNS, xmlns_both) { + rapidxml::xml_document<> doc; + doc.parse("foobar"); + std::map xmlns = { + {"x1", "p2"}, + {"x2", "p1"} + }; + auto xp = rapidxml::xpath<>::parse(xmlns,"//x1:child[text()='bar'][@attr='val2']"); + auto r = xp->first(doc); + ASSERT_TRUE(r); + EXPECT_EQ(r->name(), "child"); + EXPECT_EQ(r->value(), "bar"); +} + +TEST(XPathNS, xmlns_text_miss) { + rapidxml::xml_document<> doc; + doc.parse("foobar"); + std::map xmlns = { + {"x1", "p2"}, + {"x2", "p1"} + }; + auto xp = rapidxml::xpath<>::parse(xmlns,"//x2:child[text()='bar']"); + auto r = xp->first(doc); + ASSERT_FALSE(r); +} From 5fb95e19c61fdfd42bd40fb8637934850890571e Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Mon, 2 Sep 2024 17:17:12 +0100 Subject: [PATCH 02/11] XPath, descendants, and a splash of Sentry --- .github/workflows/gtest.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gtest.yml b/.github/workflows/gtest.yml index be18915..283c991 100644 --- a/.github/workflows/gtest.yml +++ b/.github/workflows/gtest.yml @@ -16,7 +16,21 @@ jobs: run: mkdir gtest-build - name: CMake run: cd gtest-build && cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE-CXX_FLAGS=-Werror .. + - name: Download Coverity Scan + run: curl https://scan.coverity.com/download/linux64 -d token=6Uqw2igbwL4oGX5kyFHjDg&project=dwd%2Frapidxml -o coverity.tar.gz + - name: Unpack Coverity + run: mkdir coverity && cd coverity && tar xf ../coverity.tar.gz - name: Make - run: cd gtest-build && make + run: cd gtest-build && ../coverity/cov-build --dir cov-int make - name: Run Tests run: cd gtest-build && ./rapidxml-test + - name: Tar up Coverity output + run: cd gtest-build && tar czf ../cov-build-output.tar.gz cov-int + - name: Upload it + run: | + curl --form token=${{ secrets.COVERITY_TOKEN }} \ + --form email=dave@cridland.net \ + --form file=@cov-build-output.tar.gz \ + --form version="vX" \ + --form description="RapidXML (Dave's Version)" \ + https://scan.coverity.com/builds?project=dwd%2Frapidxml From 08184c466e8ad3ea7f2e4bb075aa1d96373eb76f Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Mon, 2 Sep 2024 17:19:58 +0100 Subject: [PATCH 03/11] XPath, descendants, and a splash of Sentry --- .github/workflows/gtest.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/gtest.yml b/.github/workflows/gtest.yml index 283c991..dbbc23f 100644 --- a/.github/workflows/gtest.yml +++ b/.github/workflows/gtest.yml @@ -12,6 +12,15 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Checkout Sentry Native + uses: actions/checkout@v4 + with: + repository: getsentry/sentry-native + path: deps/sentry-native + - name: Apt dance + run: sudo apt-get update && sudo apt-get upgrade -yy + - name: Install libcurl + run: sudo apt-get install libcurl4-openssl-dev - name: Make build directory run: mkdir gtest-build - name: CMake From 468dd06a81580f5b58d302a6cd956f1459dd8171 Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Mon, 2 Sep 2024 17:23:35 +0100 Subject: [PATCH 04/11] XPath, descendants, and a splash of Sentry --- .github/workflows/gtest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gtest.yml b/.github/workflows/gtest.yml index dbbc23f..e9c0135 100644 --- a/.github/workflows/gtest.yml +++ b/.github/workflows/gtest.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 with: repository: getsentry/sentry-native - path: deps/sentry-native + path: sentry-native - name: Apt dance run: sudo apt-get update && sudo apt-get upgrade -yy - name: Install libcurl From 4ef924795f6cfc1c5ac60e3eaf23e1617097a050 Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Mon, 2 Sep 2024 17:41:58 +0100 Subject: [PATCH 05/11] XPath, descendants, and a splash of Sentry --- .github/workflows/gtest.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gtest.yml b/.github/workflows/gtest.yml index e9c0135..3b6ba00 100644 --- a/.github/workflows/gtest.yml +++ b/.github/workflows/gtest.yml @@ -26,11 +26,11 @@ jobs: - name: CMake run: cd gtest-build && cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE-CXX_FLAGS=-Werror .. - name: Download Coverity Scan - run: curl https://scan.coverity.com/download/linux64 -d token=6Uqw2igbwL4oGX5kyFHjDg&project=dwd%2Frapidxml -o coverity.tar.gz + run: curl https://scan.coverity.com/download/linux64 -d 'token=${{ secrets.COVERITY_TOKEN }}&project=dwd%2Frapidxml' -o coverity.tar.gz - name: Unpack Coverity - run: mkdir coverity && cd coverity && tar xf ../coverity.tar.gz + run: mkdir coverity && cd coverity && tar xf ../coverity.tar.gz && ln -s cov-analysis-* current - name: Make - run: cd gtest-build && ../coverity/cov-build --dir cov-int make + run: cd gtest-build && ../coverity/current/cov-build --dir cov-int make - name: Run Tests run: cd gtest-build && ./rapidxml-test - name: Tar up Coverity output From 7477f7e2274937d8c01237396acf4a48b403ddd3 Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Mon, 2 Sep 2024 18:33:48 +0100 Subject: [PATCH 06/11] XPath, descendants, and a splash of Sentry --- .github/workflows/gtest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gtest.yml b/.github/workflows/gtest.yml index 3b6ba00..4135147 100644 --- a/.github/workflows/gtest.yml +++ b/.github/workflows/gtest.yml @@ -30,7 +30,7 @@ jobs: - name: Unpack Coverity run: mkdir coverity && cd coverity && tar xf ../coverity.tar.gz && ln -s cov-analysis-* current - name: Make - run: cd gtest-build && ../coverity/current/cov-build --dir cov-int make + run: cd gtest-build && ../coverity/current/bin/cov-build --dir cov-int make - name: Run Tests run: cd gtest-build && ./rapidxml-test - name: Tar up Coverity output From 4986371fdb217d3a37907d733b800d9c85dda19d Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Mon, 2 Sep 2024 20:13:19 +0100 Subject: [PATCH 07/11] XPath, descendants, and a splash of Sentry --- rapidxml_generator.hpp | 2 +- rapidxml_predicates.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rapidxml_generator.hpp b/rapidxml_generator.hpp index 8279121..c20e17e 100644 --- a/rapidxml_generator.hpp +++ b/rapidxml_generator.hpp @@ -42,7 +42,7 @@ namespace rapidxml { }; struct handle_type : std::coroutine_handle { - explicit handle_type(std::coroutine_handle h) : std::coroutine_handle(h) {} + explicit handle_type(std::coroutine_handle && h) : std::coroutine_handle(std::move(h)) {} T &operator*() { return *(this->promise().value); diff --git a/rapidxml_predicates.hpp b/rapidxml_predicates.hpp index 3da4b5d..83f63b1 100644 --- a/rapidxml_predicates.hpp +++ b/rapidxml_predicates.hpp @@ -75,7 +75,7 @@ namespace rapidxml { bool do_match(const xml_node & t) override { if (t.type() != node_element) return false; - for (auto attr : t.attributes()) { + for (auto const & attr : t.attributes()) { if (m_xmlns.has_value()) { if (m_name == "*" || attr.local_name() != m_name) continue; if (attr.xmlns() != m_xmlns.value()) continue; From 6d7fe285ed6961d324cf5a52fef93a3f7e9f2438 Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Tue, 3 Sep 2024 13:26:37 +0100 Subject: [PATCH 08/11] XPath, descendants, and a splash of Sentry --- .github/workflows/gtest.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/gtest.yml b/.github/workflows/gtest.yml index 4135147..069e5ce 100644 --- a/.github/workflows/gtest.yml +++ b/.github/workflows/gtest.yml @@ -43,3 +43,14 @@ jobs: --form version="vX" \ --form description="RapidXML (Dave's Version)" \ https://scan.coverity.com/builds?project=dwd%2Frapidxml + - name: SonarQube install + uses: SonarSource/sonarcloud-github-c-cpp@v3 + - name: Clean build + run: cd gtest-build && make clean + - name: Build Wrapper + run: cd gtest-build && build-wrapper-linux-x86-64 --out-dir sonar-out make + - name: Sonar Scanner + run: cd gtest-build && sonar-scanner --define sonar.cfamily.compile-commands=sonar-out/compile_commands.json + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 8950fa9a4d25f6accc7a6ab7f4d19cbf229e5d2a Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Tue, 3 Sep 2024 13:33:08 +0100 Subject: [PATCH 09/11] XPath, descendants, and a splash of Sentry --- sonar-project.properties | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 sonar-project.properties diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..fd1cf74 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,13 @@ +sonar.projectKey=dwd-github_rapidxml +sonar.organization=dwd-github + +# This is the name and version displayed in the SonarCloud UI. +#sonar.projectName=RapidXML +#sonar.projectVersion=1.0 + + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +#sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 \ No newline at end of file From 1816b39677c57bf50880b5ff08f0017516e5502a Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Tue, 3 Sep 2024 14:03:45 +0100 Subject: [PATCH 10/11] XPath, descendants, and a splash of Sentry --- .github/workflows/gtest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gtest.yml b/.github/workflows/gtest.yml index 069e5ce..4ece01c 100644 --- a/.github/workflows/gtest.yml +++ b/.github/workflows/gtest.yml @@ -50,7 +50,7 @@ jobs: - name: Build Wrapper run: cd gtest-build && build-wrapper-linux-x86-64 --out-dir sonar-out make - name: Sonar Scanner - run: cd gtest-build && sonar-scanner --define sonar.cfamily.compile-commands=sonar-out/compile_commands.json + run: cd gtest-build && sonar-scanner --define sonar.cfamily.compile-commands=sonar-out/compile_commands.json --define sonar.projectKey=dwd-github_rapidxml --define sonar.organization=dwd-github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 669816f85bf4f9810451bec2e622a3fe4e5dc7ea Mon Sep 17 00:00:00 2001 From: Dave Cridland Date: Tue, 3 Sep 2024 14:19:25 +0100 Subject: [PATCH 11/11] XPath, descendants, and a splash of Sentry --- .github/workflows/gtest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/gtest.yml b/.github/workflows/gtest.yml index 4ece01c..3c0cc18 100644 --- a/.github/workflows/gtest.yml +++ b/.github/workflows/gtest.yml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Checkout Sentry Native uses: actions/checkout@v4 with: