From 79041d25caf86775643bb93086b9663290c82547 Mon Sep 17 00:00:00 2001 From: Lee *!* Clagett Date: Fri, 23 Jan 2026 21:00:18 -0500 Subject: [PATCH] Update subaddr lookahead to use new LWS lookahead feature (#3) --- .github/workflows/run_tests.yml | 1 + CMakeLists.txt | 7 +- src/backend.cpp | 539 +++++------ src/backend.h | 20 +- src/lwsf_rpc.cpp | 95 +- src/lwsf_rpc.h | 136 +-- tests/CMakeLists.txt | 29 + tests/unit/CMakeLists.txt | 37 + tests/unit/backend.test.cpp | 73 ++ tests/unit/framework.test.cpp | 37 + tests/unit/framework.test.h | 39 + tests/unit/lest.hpp | 1487 +++++++++++++++++++++++++++++++ tests/unit/main.cpp | 33 + tests/unit/rpc.test.cpp | 159 ++++ 14 files changed, 2312 insertions(+), 380 deletions(-) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/unit/CMakeLists.txt create mode 100644 tests/unit/backend.test.cpp create mode 100644 tests/unit/framework.test.cpp create mode 100644 tests/unit/framework.test.h create mode 100644 tests/unit/lest.hpp create mode 100644 tests/unit/main.cpp create mode 100644 tests/unit/rpc.test.cpp diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 09efa46..e4a0df0 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -69,6 +69,7 @@ jobs: msystem: MINGW64 update: true install: > + coreutils mingw-w64-x86_64-boost mingw-w64-x86_64-cmake mingw-w64-x86_64-gcc diff --git a/CMakeLists.txt b/CMakeLists.txt index d2713bd..6bf813b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,8 @@ enable_language(CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +option(LWSF_BUILD_TESTS "Build Tests" OFF) + if (NOT MONERO_SOURCE_DIR) message(FATAL_ERROR "The argument -DMONERO_SOURCE_DIR must specify a location of a monero source tree") endif() @@ -90,4 +92,7 @@ set_property(TARGET monero::libraries PROPERTY ) add_subdirectory(src) - +if (LWSF_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/src/backend.cpp b/src/backend.cpp index 8a4e4bb..3eabc3a 100644 --- a/src/backend.cpp +++ b/src/backend.cpp @@ -154,7 +154,7 @@ namespace lwsf { namespace internal { namespace backend { return !lookahead.major && !lookahead.minor && subaccounts.size() == 1 && !subaccounts[0].last; } - + template void map_address_book_entry(F& format, T& self) { @@ -469,17 +469,12 @@ namespace lwsf { namespace internal { namespace backend struct merge_results { std::vector> new_transactions; - boost::container::flat_set> new_subaddrs; + boost::container::flat_set new_subaddrs; + boost::optional lookahead_fail; void merge_subaddr(const rpc::address_meta& meta) { - auto elem = new_subaddrs.find(meta.maj_i); - if (elem == new_subaddrs.end()) - elem = new_subaddrs.insert(rpc::subaddrs{meta.maj_i}).first; - - const auto existing = elem->head; - std::get<0>(elem->head) = std::min(std::get<0>(existing), meta.min_i); - std::get<1>(elem->head) = std::max(std::get<1>(existing), meta.min_i); + new_subaddrs.insert(meta); } }; @@ -488,6 +483,7 @@ namespace lwsf { namespace internal { namespace backend // Remember that this function provides the strong exception guarantee. merge_results out; + out.lookahead_fail = source.lookahead_fail; /* Backend server could remove or modify txes (rescan or bug fix); the easiest way to handle this is to start a new copy of the txes. This is @@ -588,13 +584,13 @@ namespace lwsf { namespace internal { namespace backend // udpate "serer_lookahead" values later, we force lookahead from zero for (const auto& sub : out.new_subaddrs) { - if (std::numeric_limits::max() <= sub.key) + if (std::numeric_limits::max() <= sub.maj_i) throw std::runtime_error{"merge_response exceeded max size_t value"}; - if (self.primary.subaccounts.size() <= sub.key) - self.primary.subaccounts.resize(std::size_t(sub.key) + 1); + if (self.primary.subaccounts.size() <= sub.maj_i) + self.primary.subaccounts.resize(std::size_t(sub.maj_i) + 1); - auto& acct = self.primary.subaccounts.at(sub.key); - acct.last = std::max(acct.last, std::get<1>(sub.head)); + auto& acct = self.primary.subaccounts.at(sub.maj_i); + acct.last = std::max(acct.last, sub.min_i); } // Update txes _after_ subaccounts table @@ -603,6 +599,8 @@ namespace lwsf { namespace internal { namespace backend self.blockchain_height = source.blockchain_height; self.primary.restore_height = source.start_height; self.primary.scan_height = source.scanned_block_height; + self.server_lookahead.major = source.lookahead.maj_i; + self.server_lookahead.minor = source.lookahead.min_i; self.fee_mask = unspents.fee_mask; if (unspents.fees.empty()) @@ -611,101 +609,12 @@ namespace lwsf { namespace internal { namespace backend self.per_byte_fee[0] = unspents.per_byte_fee; } else - self.per_byte_fee = unspents.fees; + self.per_byte_fee = std::move(unspents.fees); return out; } - /*! Update `server_lookahead` minor fields based on `all`. The top-level - major lookahead is set to error state if `false` is returned, and set to - the server lookahead iff true is returned. - \return True iff the server has enough lookahead for all fields. */ - bool update_lookaheads(wallet& self, const boost::container::flat_set>& all) noexcept - { - self.server_lookahead = 0; - self.lookahead_error = {}; - - const auto lookahead = self.primary.lookahead; - if (no_subaddresses(epee::to_span(self.primary.subaccounts), lookahead)) - return true; - - static_assert(std::is_samekey)>()); - const std::uint32_t server_lookahead = all.empty() ? - std::uint32_t(0) : all.rbegin()->key; - - // Ensure major lookahead starts at zero, has no gaps, and "far" enough. - const std::size_t last_account = std::max(self.primary.subaccounts.size(), std::size_t(1)) - 1; - bool min_met = add_uint32_clamp(last_account, lookahead.major) <= server_lookahead; - min_met &= all.size() == std::uint64_t(server_lookahead) + 1; - - // track the min lookahead we have - if (!min_met) - { - std::uint32_t last = -1; - for (const rpc::subaddrs& e : all) - { - if (last + 1 == e.key) - self.server_lookahead = ++last; - else - break; - } - } - else - self.server_lookahead = server_lookahead; - - for (std::size_t i = 0; i < self.primary.subaccounts.size(); ++i) - { - auto& acct = self.primary.subaccounts[i]; - const auto elem = all.find(i); - const bool has_range = elem != all.end(); - - const std::uint32_t this_lookahead = has_range ? std::get<1>(elem->head) : 0; - min_met &= add_uint32_clamp(acct.last, lookahead.minor) <= this_lookahead; - min_met &= !lookahead.minor || (has_range && std::get<0>(elem->head) == 0); - - acct.server_lookahead = this_lookahead; - } - - // Ensure that unused minor lookahead is far enough - const std::uint32_t last_minor = std::max(lookahead.minor, std::uint32_t(1)) - 1; - for (std::size_t i = self.primary.subaccounts.size(); i < all.size(); ++i) - { - if (!min_met) - break; - - const auto& range = all.nth(i)->head; - min_met &= last_minor <= std::get<1>(range); - min_met &= std::get<0>(range) == 0; - } - - if (!min_met) - self.lookahead_error = {error::subaddr_ahead}; - return min_met; - } - - void fill_upsert(const wallet& self, boost::container::flat_set>& out) - { - static constexpr const std::uint32_t max_index = - std::numeric_limits::max(); - - // remember upsert_subaddrs is inclusive in the minor ranges - - const auto& accts = self.primary.subaccounts; - const std::size_t end = accts.size() + self.primary.lookahead.major; - out.reserve(end); - - for (std::size_t i = 0; i < accts.size() && i <= max_index; ++i) - out.insert(out.end(), rpc::subaddrs{std::uint32_t(i)})->head = {0, std::max(std::uint32_t(1), add_uint32_clamp(accts[i].last, self.primary.lookahead.minor)) - 1}; - - /* `0 < lookahead.major & lookahead.minor == 0` is a strange edge case. - Assume `lookahead.minor == 1` in that scenario. */ - - const std::uint32_t minor = std::max(std::uint32_t(1), self.primary.lookahead.minor) - 1; - for (std::size_t i = accts.size(); i < end && i <= max_index; ++i) - out.insert(out.end(), rpc::subaddrs{std::uint32_t(i)})->head = {0, minor}; - } - - std::error_code handle_lookahead_error(std::error_code error) noexcept + std::error_code handle_subaddress_error(std::error_code error) noexcept { if (error == rpc_max_subaddresses) error = {error::subaddr_ahead}; @@ -722,7 +631,34 @@ namespace lwsf { namespace internal { namespace backend // Enforce special account {0,0} exists and is labeled self.detail.try_emplace(0).first->second.label = std::string{config::default_primary_name}; self.last = 0; - self.server_lookahead = 0; + } + + bool should_attempt_rescan(const account& self, boost::container::flat_map subaddrs, const std::uint64_t max_subaddresses) + { + return self.needed_subaddresses(std::move(subaddrs)) <= max_subaddresses; + } + + std::error_code prep_subs(rpc::http_client& client, const boost::string_ref client_prefix, const rpc::login& creds, std::vector required_subs, const boost::optional force_lookahead) + { + if (std::numeric_limits::max() < required_subs.size()) + throw std::runtime_error{"prep_subs exceeded max major addresses"}; + + const rpc::address_meta lookahead = force_lookahead.value_or(rpc::address_meta{}); + + rpc::upsert_subaddrs_request request{creds}; + const std::uint32_t stop = add_uint32_clamp(required_subs.size(), lookahead.maj_i); + bool needed = bool(stop); + for (std::size_t i = 0; i < stop; ++i) + { + std::uint32_t last = i < required_subs.size() ? required_subs[i].last : 0; + last = add_uint32_clamp(last, lookahead.min_i); + if (last) + request.subaddrs_.emplace(std::uint32_t(i), rpc::subaddrs{last}); + needed |= bool(last); + } + if (!needed) + return {}; + return rpc::invoke(client, client_prefix, request).error(); } } // anonymous @@ -747,8 +683,75 @@ namespace lwsf { namespace internal { namespace backend void write_bytes(wire::writer& dest, const account& source) { map_account(dest, source); } + std::uint64_t account::needed_subaddresses(boost::container::flat_map subaddrs) const + { + if (this->lookahead.major == 0 || this->lookahead.minor == 0) + return 0; + + std::uint64_t count = 0; + const auto add_subaddresses = [&count] (const std::uint64_t next) + { + if (next <= std::numeric_limits::max() - count) + count += next; + else + count = std::numeric_limits::max(); + }; + + const auto count_subaddresses = [this, &add_subaddresses] (const boost::container::flat_set, std::less<>>& minors) + { + auto last = minors.begin(); + while (last != minors.end() && std::get<1>(*last) < this->lookahead.minor) + ++last; + + // tally minium first, as determined by minor lookahead + { + std::uint64_t next = this->lookahead.minor; + if (last != minors.end()) + next = std::max(next, std::uint64_t(std::get<1>(*last)) + 1); + + add_subaddresses(next); + } + + // last was just included in total + if (last != minors.end()) + ++last; + + // tally any upserted out of lookahead range + for ( ; last != minors.end(); ++last) + add_subaddresses(std::uint64_t(std::get<1>(*last)) - std::uint64_t(std::get<0>(*last)) + 1); + }; + + if (this->subaccounts.empty()) + throw std::runtime_error{"Subaccounts has invalid state"}; + + // all registered subaddresses + for (std::size_t i = 0; i < this->subaccounts.size(); ++i) + { + if (std::numeric_limits::max() < i) + throw std::runtime_error{"Invalid subaddress major index"}; + + auto major = subaddrs.emplace(std::uint32_t(i), rpc::subaddrs{}).first; + major->second.merge(add_uint32_clamp(this->subaccounts[i].last, this->lookahead.minor - 1)); + count_subaddresses(major->second.value); + } + + const std::uint32_t last = + add_uint32_clamp(this->subaccounts.size() - 1, this->lookahead.major); + for (std::size_t i = this->subaccounts.size(); i < last; ++i) + add_subaddresses(this->lookahead.minor); + + // go through subaddresses after main lookahead + for (auto elem = subaddrs.lower_bound(this->lookahead.major); elem != subaddrs.end(); ++elem) + { + for (const auto& minor : elem->second.value) + add_subaddresses(std::uint64_t(std::get<1>(minor)) - std::uint64_t(std::get<0>(minor)) + 1); + } + + return count; + } + sub_account::sub_account() - : detail(), last(0), server_lookahead(0) + : detail(), last(0) {} std::string_view sub_account::sub_label(const std::uint32_t minor) const noexcept @@ -830,7 +833,7 @@ namespace lwsf { namespace internal { namespace backend last_sync(), blockchain_height(0), fee_mask(0), - server_lookahead(0), + server_lookahead{}, sync(), sync_listener(), sync_refresh(), @@ -881,6 +884,13 @@ namespace lwsf { namespace internal { namespace backend ); } + bool wallet::lookahead_good() const noexcept + { + return + primary.lookahead.major <= server_lookahead.major && + primary.lookahead.minor <= server_lookahead.minor; + } + expect wallet::to_bytes() const { epee::byte_stream dest{}; @@ -915,76 +925,81 @@ namespace lwsf { namespace internal { namespace backend bool new_account = false; boost::unique_lock lock{sync}; { + if (passed_login) + return false; // cannot be new account anymore + passed_login = false; + probed_lookahead = false; per_byte_fee.clear(); fee_mask = 0; - server_lookahead = 0; - import_error = {}; - lookahead_error = error::subaddr_disabled; - const rpc::login_request login{ - primary.address, primary.view.sec, true, primary.generated_locally + server_lookahead = {}; + refresh_error = {}; + subaddress_error = {}; + import_error = {}; + lookahead_error = lookahead_good() ? std::error_code{} : error::subaddr_disabled; + + rpc::login_request login{ + primary.address, primary.view.sec, primary.lookahead, true, primary.generated_locally }; - lock.unlock(); - const auto response = rpc::invoke(*client, client_prefix, login); - if (!response) + for (unsigned i = 0; i < 2; ++i) { - if (response == rpc_unapproved) - return {error::approval}; - else if (response == rpc_internal_error) - return {error::network_type}; // almost always this - else if (response == rpc_not_implemented) - return {error::create}; - return response.error(); - } - lock.lock(); + /* Do not release `lock` during login calls, want to temporarily block + everything until complete or timeout. */ - passed_login = true; - if (response->start_height) - primary.restore_height = *response->start_height; + const auto response = rpc::invoke(*client, client_prefix, login); + if (!response) + { + if (response == rpc_unapproved) + return {error::approval}; + else if (response == rpc_internal_error) + return {error::network_type}; // almost always this + else if (response == rpc_not_implemented) + return {error::create}; + else if (0 < i) + return response.error(); + login.lookahead = {}; + } + else // response + { + new_account = response->new_address; + passed_login = true; - if (no_subaddresses(epee::to_span(primary.subaccounts), primary.lookahead)) - { - lookahead_error = {}; - return response->new_address; + if (response->start_height) + primary.restore_height = *response->start_height; + + if (response->lookahead) + { + server_lookahead = {response->lookahead->maj_i, response->lookahead->min_i}; + lookahead_error = {}; + } + + /* Make sure all registered subs are known to this backend. This can + differ from lookahead because API allows arbitrary major,minor + requests to be performed. */ + auto subs = primary.subaccounts; + boost::optional force_lookahead; + if (!response->lookahead) + force_lookahead = primary.lookahead; + lock.unlock(); + const std::error_code error = + prep_subs(*client, client_prefix, {login.address, login.view_key}, std::move(subs), force_lookahead); + lock.lock(); + if (error && !subaddress_error) + subaddress_error = handle_subaddress_error(error); + + break; // retry loop + } } - new_account = response->new_address; - } - - // Converting `rpc::invoke` into exceptions is easier here - try - { - rpc::login login{primary.address, primary.view.sec}; - { - lock.unlock(); - const auto response = rpc::invoke(*client, client_prefix, login).value(); - lock.lock(); - if (update_lookaheads(*this, response.all_subaddrs)) - return new_account; - } - - // lookaheads not far enough - rpc::upsert_subaddrs_request request{std::move(login), {}, true /* get_all */}; - fill_upsert(*this, request.subaddrs_); - - lock.unlock(); - const auto response = rpc::invoke(*client, client_prefix, request).value(); - lock.lock(); - update_lookaheads(*this, response.all_subaddrs); - } - catch (const std::system_error& e) - { - if (e.code() == rpc_unapproved) // happens on new account creation - return {error::approval}; - else - lookahead_error = handle_lookahead_error(e.code()); } + if (!lookahead_good() && !lookahead_error) + lookahead_error = {error::subaddr_ahead}; return new_account; - } + } std::error_code wallet::refresh(const bool mandatory) - { + { // Remember that this function provides the strong exception guarantee. boost::unique_lock lock_refresh{sync_refresh}; @@ -1048,81 +1063,51 @@ namespace lwsf { namespace internal { namespace backend const std::uint64_t orig_scan_height = primary.scan_height; auto merged = merge_response(*this, *txs_response, *outs_response); - if (import_error || primary.requested_start < primary.restore_height) + if (merged.lookahead_fail || !lookahead_good()) { - const std::uint64_t from_height = primary.requested_start; - lock.unlock(); - restore_height(from_height); - lock.lock(); + const std::uint64_t height = merged.lookahead_fail.value_or(this->primary.restore_height); + if (!lookahead_error) + lookahead_error = {error::subaddr_ahead}; + if (!this->probed_lookahead) + { + this->probed_lookahead = true; // check just once for re-scan + const auto info = rpc::invoke(*client, client_prefix, rpc::empty{}); + if (info) + { + const auto subaddrs = rpc::invoke(*client, client_prefix, login); + if (subaddrs && should_attempt_rescan(primary, std::move(subaddrs->all_subaddrs), info->max_subaddresses)) + { + lock.unlock(); + restore_height(height); + lock.lock(); + } + } + else + lookahead_error = info.error(); + } + } + else if (lookahead_good()) + { + lookahead_error = {}; + probed_lookahead = false; + if (primary.restore_height <= primary.requested_start) + import_error = {}; } - if (lookahead_error) + if (!import_error && primary.requested_start < primary.restore_height) { - rpc::upsert_subaddrs_request request{login, {}, true /* get_all */}; - fill_upsert(*this, request.subaddrs_); - + const auto height = primary.requested_start; lock.unlock(); - const auto response = rpc::invoke(*client, client_prefix, request); + restore_height(height); lock.lock(); - if (response && update_lookaheads(*this, response->all_subaddrs)) - lookahead_error = {}; } - if (!merged.new_subaddrs.empty() && !lookahead_error) - { - server_lookahead = std::max(server_lookahead, merged.new_subaddrs.rbegin()->key); - - // ensure lookahead is from zero, or our logic is busted - for (auto& sub : merged.new_subaddrs) - { - std::get<0>(sub.head) = 0; - std::get<1>(sub.head) = add_uint32_clamp(std::get<1>(sub.head), primary.lookahead.minor); - } - - const rpc::upsert_subaddrs_request upsert_request{ - login, std::move(merged.new_subaddrs), false /* get_all */ - }; - - lock.unlock(); - const auto upsert_response = rpc::invoke(*client, client_prefix, upsert_request); - lock.lock(); - - if (upsert_response) - { - for (const auto& sub : upsert_request.subaddrs_) - { - auto& acct = primary.subaccounts.at(sub.key); - acct.server_lookahead = std::max(acct.server_lookahead, std::get<1>(sub.head)); - } - - if (primary.lookahead.major) - { - const std::uint32_t last_used = upsert_request.subaddrs_.rbegin()->key; - const std::uint32_t maj_i = add_uint32_clamp(unsigned(1), last_used); - const std::uint32_t maj_count = primary.lookahead.major; - const std::uint32_t min_count = std::max(std::uint32_t(1), primary.lookahead.minor); - const rpc::provision_subaddrs_request provision_request{ - login, maj_i, 0, maj_count, min_count, false /* get_all */ - }; - - lock.unlock(); - const auto provision_response = rpc::invoke(*client, client_prefix, provision_request); - lock.lock(); - - if (provision_response) - server_lookahead = std::max(server_lookahead, add_uint32_clamp(last_used, maj_count)); - else // if !provision_response - lookahead_error = handle_lookahead_error(provision_response.error()); - } - } - else // if !upsert_response - lookahead_error = handle_lookahead_error(upsert_response.error()); - } // if new subaddress(es) - // return error if subaddresses enabled, and recovered wallet refresh_on_exit.release(); // release before acquiring `sync_listener`. const std::error_code rc = refresh_error = - import_error ? import_error : lookahead_error; + import_error ? + import_error : subaddress_error ? + subaddress_error : lookahead_error; const boost::lock_guard lock_listener{sync_listener}; if (!listener) return rc; @@ -1173,26 +1158,19 @@ namespace lwsf { namespace internal { namespace backend lock.lock(); } - const std::uint32_t needed_maj_i = add_uint32_clamp(maj_i, primary.lookahead.major); - if (!lookahead_error && needed_maj_i <= server_lookahead) - return {}; - - const std::uint32_t major_count = needed_maj_i - server_lookahead; const std::uint32_t minor_count = std::max(std::uint32_t(1), primary.lookahead.minor); const rpc::provision_subaddrs_request request{ rpc::login{primary.address, primary.view.sec}, - server_lookahead + 1, 0, major_count, minor_count, false /* get_all */ + maj_i, 0, 1, minor_count, false /* get_all */ }; lock.unlock(); const auto response = rpc::invoke(*client, client_prefix, request); lock.lock(); - if (response) - server_lookahead = std::max(server_lookahead, needed_maj_i); - else if (!lookahead_error) - lookahead_error = handle_lookahead_error(response.error()); - return lookahead_error; + if (!response && !subaddress_error) + subaddress_error = handle_subaddress_error(response.error()); + return subaddress_error; } std::error_code wallet::register_subaddress(const std::uint32_t maj_i, const std::uint32_t min_i) @@ -1201,8 +1179,7 @@ namespace lwsf { namespace internal { namespace backend if (primary.subaccounts.size() <= maj_i) throw std::invalid_argument{"wallet::register_subaddress exceeded major index"}; - auto maj = std::addressof(primary.subaccounts.at(maj_i)); - if (maj->last < min_i) + if (primary.subaccounts.at(maj_i).last < min_i) throw std::invalid_argument{"wallet::register_subaddress exceeded minor index"}; if (!passed_login) @@ -1215,9 +1192,6 @@ namespace lwsf { namespace internal { namespace backend } const std::uint32_t needed_min_i = add_uint32_clamp(min_i, primary.lookahead.minor); - if (!lookahead_error && needed_min_i <= maj->server_lookahead) - return {}; - const std::uint32_t needed_count = add_uint32_clamp(unsigned(1), needed_min_i); const rpc::provision_subaddrs_request request{ rpc::login{primary.address, primary.view.sec}, @@ -1228,52 +1202,20 @@ namespace lwsf { namespace internal { namespace backend const auto response = rpc::invoke(*client, client_prefix, request); lock.lock(); - if (response) - { - // address could've changed during unlock - maj = std::addressof(primary.subaccounts.at(maj_i)); - maj->server_lookahead = std::max(maj->server_lookahead, needed_min_i); - } - else if (!lookahead_error) - lookahead_error = handle_lookahead_error(response.error()); - return lookahead_error; + if (!response && !subaddress_error) + subaddress_error = handle_subaddress_error(response.error()); + return subaddress_error; } std::error_code wallet::set_lookahead(std::uint32_t major, std::uint32_t minor) { boost::unique_lock lock{sync}; - const bool extending = - primary.lookahead.major < major || - primary.lookahead.minor < minor; - primary.lookahead.major = major; primary.lookahead.minor = minor; - if (!passed_login) - { - lock.unlock(); - const std::error_code error = login(); - if (error) - return error; - lock.lock(); - } - - if (!extending && !lookahead_error) - return {}; - - rpc::upsert_subaddrs_request request{ - rpc::login{primary.address, primary.view.sec}, {}, true /* get_all */ - }; - fill_upsert(*this, request.subaddrs_); - + const std::uint64_t from_height = primary.scan_height; lock.unlock(); - const auto response = rpc::invoke(*client, client_prefix, request); - lock.lock(); - if (!response) - return (lookahead_error = handle_lookahead_error(response.error())); - - update_lookaheads(*this, response->all_subaddrs); - return {}; + return restore_height(from_height).error(); } expect wallet::restore_height(const std::uint64_t height) @@ -1281,19 +1223,27 @@ namespace lwsf { namespace internal { namespace backend const auto update_import = [this](expect value) { if (value) + { this->import_error = {}; + if (value->lookahead) + { + this->server_lookahead = {value->lookahead->maj_i, value->lookahead->min_i}; + this->lookahead_error = {}; + } + else + this->lookahead_error = {error::subaddr_disabled}; + } else - this->import_error = value.error(); + { + if (value == rpc_max_subaddresses) + this->import_error = {error::subaddr_ahead}; + else + this->import_error = value.error(); + } return value; }; boost::unique_lock lock{sync}; - - if (primary.restore_height <= height) - return update_import(rpc::import_response{}); - if (import_error && import_error == error::import_pending) - return import_error; - if (!passed_login) { lock.unlock(); @@ -1303,17 +1253,16 @@ namespace lwsf { namespace internal { namespace backend lock.lock(); } - // current api does not allow height selection, defaults to 0 - rpc::import_request login{{primary.address, primary.view.sec}, height}; + if (primary.restore_height <= height && lookahead_good() && !lookahead_error) + return update_import(rpc::import_response{.lookahead = rpc::address_meta{server_lookahead}}); + + rpc::import_request login{{primary.address, primary.view.sec}, height, primary.lookahead}; lock.unlock(); auto import = rpc::invoke(*client, client_prefix, login); lock.lock(); - if (!import) - return update_import(import.error()); - - if (import->request_fulfilled) + if (!import || import->request_fulfilled) return update_import(std::move(import)); const unsigned total = @@ -1360,7 +1309,7 @@ namespace lwsf { namespace internal { namespace backend else primary.addressbook[i] = address_book_entry{std::move(*import->payment_address), std::move(payment_id), std::move(description)}; #endif - return update_import(std::move(import)); + return update_import({error::import_pending}); } expect> wallet::get_decoys(const rpc::get_random_outs_request& req) diff --git a/src/backend.h b/src/backend.h index dbe6210..276a595 100644 --- a/src/backend.h +++ b/src/backend.h @@ -97,7 +97,6 @@ namespace lwsf { namespace internal { namespace backend { boost::container::flat_map detail; //!< Minor address info std::uint32_t last; //!< Last minor index in use (inclusive) - std::uint32_t server_lookahead; //!< Status of server (minor) lookahead. Inclusive //! Creates 1 subaddress entry (key == `0`) with default label sub_account(); @@ -203,6 +202,9 @@ namespace lwsf { namespace internal { namespace backend { account() = delete; + //! \return # subaddresses needed for `lookahead, `txes`, and `subaddrs`. + std::uint64_t needed_subaddresses(boost::container::flat_map subaddrs) const; + struct polyseed { epee::byte_slice seed; @@ -237,16 +239,18 @@ namespace lwsf { namespace internal { namespace backend std::string client_prefix; std::vector per_byte_fee; //!< by priority level std::error_code refresh_error; //!< Cached because `refresh(...)` is rate limited + std::error_code subaddress_error; //!< Errors with subaddresses (not via lookahead) std::error_code lookahead_error; //!< warnings/errors of `server_lookahead` value std::error_code import_error; //!< Error from `import_wallet_request` std::chrono::steady_clock::time_point last_sync; std::uint64_t blockchain_height; std::uint64_t fee_mask; - std::uint32_t server_lookahead; //!< Status of major lookahead server-side. Inclusive + config::lookahead server_lookahead; //!< Status of lookahead server-side mutable boost::mutex sync; boost::mutex sync_listener; boost::mutex sync_refresh; bool passed_login; + bool probed_lookahead; //!< True iff queries were done to determine lookahead re-sync wallet(); @@ -257,6 +261,7 @@ namespace lwsf { namespace internal { namespace backend cryptonote::account_keys get_primary_keys() const; cryptonote::account_public_address get_spend_account(const rpc::address_meta& index) const; std::string get_spend_address(const rpc::address_meta& index) const; + bool lookahead_good() const noexcept; // End GROUP @@ -268,17 +273,14 @@ namespace lwsf { namespace internal { namespace backend //! De-serialize `this` from msgpack. Locks+replaces contents. std::error_code from_bytes(epee::byte_slice source); - /*! Attempt login and sync subaddresses. The result of subaddress syncing, - including errors, is stored in `server_lookahead`. - \return No errors if login succeeded, and true if new account */ + /*! Attempt login and check lookahead status. Updates `lookahead_error`. + \return No errors if login succeeded, and true if new account */ expect login_is_new(); - /*! Attempt login and sync subaddresses. The result of subaddress syncing, - including errors, is stored in `server_lookahead`. - \return No errors if login succeeded. */ + /*! Attempt login and check lookahead status. Updates `lookahead_error`. + \return No errors if login succeeded. */ std::error_code login() { return login_is_new().error(); } - //! Refreshes txes information. Strong exception guarantee. std::error_code refresh(bool mandatory = false); diff --git a/src/lwsf_rpc.cpp b/src/lwsf_rpc.cpp index 5bea427..293cde7 100644 --- a/src/lwsf_rpc.cpp +++ b/src/lwsf_rpc.cpp @@ -44,6 +44,7 @@ #include "wire/json.h" #include "wire/traits.h" #include "wire/wrapper/array.h" +#include "wire/wrapper/defaulted.h" #include "wire/wrapper/trusted_array.h" #include "wire/wrappers_impl.h" @@ -143,6 +144,7 @@ namespace lwsf { namespace internal { namespace rpc wire::object(dest, WIRE_FIELD(address), WIRE_FIELD(view_key), + WIRE_FIELD_DEFAULTED(lookahead, address_meta{}), WIRE_FIELD(create_account), WIRE_FIELD(generated_locally) ); @@ -150,7 +152,11 @@ namespace lwsf { namespace internal { namespace rpc void read_bytes(wire::json_reader& source, login_response& self) { - wire::object(source, WIRE_OPTIONAL_FIELD(start_height), WIRE_FIELD(new_address)); + wire::object(source, + WIRE_OPTIONAL_FIELD(start_height), + WIRE_OPTIONAL_FIELD(lookahead), + WIRE_FIELD(new_address) + ); } void read_bytes(wire::json_reader& source, daemon_status& self) @@ -251,14 +257,21 @@ namespace lwsf { namespace internal { namespace rpc } } + void read_bytes(wire::json_reader& source, get_version& self) + { + wire::object(source, WIRE_FIELD_DEFAULTED(max_subaddresses, unsigned(0))); + } + void read_bytes(wire::json_reader& source, get_address_txs& self) { using min_tx_size = wire::min_element_size; wire::object(source, WIRE_FIELD_ARRAY(transactions, min_tx_size), + WIRE_OPTIONAL_FIELD(lookahead_fail), WIRE_FIELD(scanned_block_height), WIRE_FIELD(start_height), - WIRE_FIELD(blockchain_height) + WIRE_FIELD(blockchain_height), + WIRE_FIELD_DEFAULTED(lookahead, address_meta{}) ); } @@ -340,19 +353,72 @@ namespace lwsf { namespace internal { namespace rpc { return {std::move(val)}; } template - void map_subaddrs(F& format, T& self) + void map_subaddr(F& format, T& self) { - auto head = array_head(std::ref(self.head)); wire::object(format, - WIRE_FIELD(key), - wire::field("value", wire::trusted_array(std::ref(head))) + wire::field("key", std::ref(self.first)), + wire::field("value", std::ref(self.second)) ); - if (head.empty()) - WIRE_DLOG_THROW(wire::error::schema::array, "unexpected empty array"); } } - - WIRE_JSON_DEFINE_OBJECT(subaddrs, map_subaddrs); + + bool subaddrs::is_valid() const noexcept + { + std::int64_t last = -1; + for (const auto& elem : value) + { + if (std::get<1>(elem) < std::get<0>(elem)) + return false; + if (std::int64_t(std::get<0>(elem)) <= last) + return false; + if (std::int64_t(std::get<1>(elem)) <= last) + return false; + last = std::get<1>(elem); + } + return true; + } + + void subaddrs::merge(const std::uint32_t index) + { + auto start = value.lower_bound({index, 0}); + if (start != value.end()) + { // merge lower ranges + if (index < std::get<0>(*start)) + { + if (start == value.begin()) + { + value.insert({0, index}); + return; + } + --start; + } + + const auto highest = std::max(index, std::get<1>(*start)); + value.erase(value.begin(), start + 1); + value.insert({0, highest}); + } + else if (start == value.end()) + { + auto highest = index; + if (!value.empty()) + highest = std::max(highest, std::get<1>(*value.rbegin())); + value.clear(); + value.insert({0, highest}); + } + } + + void read_bytes(wire::json_reader& source, subaddrs& self) + { + wire_read::array(source, self.value, wire::min_element_size<2>{}); + if (!self.is_valid()) + WIRE_DLOG_THROW(wire::error::schema::array, "invalid array mapping"); + } + void write_bytes(wire::json_writer& dest, const subaddrs& self) + { + wire_write::array(dest, self.value); + } + + WIRE_JSON_DEFINE_OBJECT(get_subaddrs::mapped_type, map_subaddr); void read_bytes(wire::json_reader& source, get_subaddrs& self) { wire::object(source, WIRE_FIELD_ARRAY(all_subaddrs, max_subaddrs)); @@ -455,7 +521,8 @@ namespace lwsf { namespace internal { namespace rpc wire::object(source, wire::field("address", std::cref(self.creds.address)), wire::field("view_key", std::cref(self.creds.view_key)), - WIRE_FIELD(from_height) + WIRE_FIELD_DEFAULTED(from_height, unsigned(0)), + WIRE_FIELD_DEFAULTED(lookahead, address_meta{}) ); } @@ -465,6 +532,7 @@ namespace lwsf { namespace internal { namespace rpc WIRE_OPTIONAL_FIELD(payment_address), WIRE_OPTIONAL_FIELD(payment_id), WIRE_OPTIONAL_FIELD(import_fee), + WIRE_OPTIONAL_FIELD(lookahead), WIRE_FIELD(status), WIRE_FIELD(new_request), WIRE_FIELD(request_fulfilled) @@ -517,10 +585,7 @@ namespace lwsf { namespace internal { namespace rpc void read_bytes(wire::json_reader& source, upsert_subaddrs_response& self) { - wire::object(source, - WIRE_FIELD_ARRAY(new_subaddrs, max_subaddrs), - WIRE_FIELD_ARRAY(all_subaddrs, max_subaddrs) - ); + wire::object(source); } }}} // lwsf // internal // rpc diff --git a/src/lwsf_rpc.h b/src/lwsf_rpc.h index d7ad750..427cc03 100644 --- a/src/lwsf_rpc.h +++ b/src/lwsf_rpc.h @@ -28,6 +28,7 @@ #pragma once +#include #include #include #include @@ -39,6 +40,7 @@ #include "byte_stream.h" // moneor/contrib/epee/include #include "common/expect.h" // monero/src #include "crypto/crypto.h" // monero/src +#include "lwsf_config.h" #include "ringct/rctTypes.h" // monero/src #include "wire/basic_value.h" #include "wire/fwd.h" @@ -53,7 +55,7 @@ namespace epee { namespace net_utils namespace lwsf { namespace internal { namespace rpc { - using max_subaddrs = wire::max_element_count<43690>; + using max_subaddrs = wire::max_element_count<16384>; using http_client = epee::net_utils::http::http_simple_client_template< epee::net_utils::blocked_mode_client >; @@ -93,6 +95,42 @@ namespace lwsf { namespace internal { namespace rpc struct empty {}; WIRE_DECLARE_OBJECT(empty); + + struct address_meta + { + std::uint32_t maj_i; + std::uint32_t min_i; + + constexpr address_meta() noexcept + : maj_i(0), min_i(0) + {} + + constexpr address_meta(const std::uint32_t maj, std::uint32_t min) noexcept + : maj_i(maj), min_i(min) + {} + + constexpr address_meta(config::lookahead src) noexcept + : maj_i(src.major), min_i(src.minor) + {} + + constexpr bool is_default() const noexcept { return !maj_i && !min_i; } + }; + WIRE_DECLARE_OBJECT(address_meta); + + inline constexpr bool operator<(const address_meta& lhs, const address_meta& rhs) noexcept + { + return lhs.maj_i == rhs.maj_i ? + lhs.min_i < rhs.min_i : lhs.maj_i < rhs.maj_i; + } + inline constexpr bool operator==(const address_meta& lhs, const address_meta& rhs) noexcept + { + return lhs.maj_i == rhs.maj_i && lhs.min_i == rhs.min_i; + } + inline constexpr bool operator!=(const address_meta& lhs, const address_meta& rhs) noexcept + { + return lhs.maj_i != rhs.maj_i || lhs.min_i != rhs.min_i; + } + struct login { @@ -110,6 +148,7 @@ namespace lwsf { namespace internal { namespace rpc std::string address; crypto::secret_key view_key; + address_meta lookahead; bool create_account; bool generated_locally; }; @@ -121,6 +160,7 @@ namespace lwsf { namespace internal { namespace rpc static constexpr const char* endpoint() noexcept { return "/login"; } boost::optional start_height; + boost::optional lookahead; bool new_address; }; void read_bytes(wire::json_reader&, login_response&); @@ -141,34 +181,7 @@ namespace lwsf { namespace internal { namespace rpc enum class uint64_string : std::uint64_t {}; void write_bytes(wire::json_writer&, uint64_string); void read_bytes(wire::json_reader&, uint64_string&); - - struct address_meta - { - std::uint32_t maj_i; - std::uint32_t min_i; - - constexpr address_meta() noexcept - : maj_i(0), min_i(0) - {} - - constexpr address_meta(const std::uint32_t maj, std::uint32_t min) noexcept - : maj_i(maj), min_i(min) - {} - - constexpr bool is_default() const noexcept { return !maj_i && !min_i; } - }; - WIRE_DECLARE_OBJECT(address_meta); - - inline constexpr bool operator<(const address_meta& lhs, const address_meta& rhs) noexcept - { - return lhs.maj_i == rhs.maj_i ? - lhs.min_i < rhs.min_i : lhs.maj_i < rhs.maj_i; - } - inline constexpr bool operator==(const address_meta& lhs, const address_meta& rhs) noexcept - { - return lhs.maj_i == rhs.maj_i && lhs.min_i == rhs.min_i; - } - + struct transaction_spend { uint64_string amount; @@ -221,13 +234,25 @@ namespace lwsf { namespace internal { namespace rpc static constexpr const char* endpoint() noexcept { return "/get_address_txs"; } std::vector transactions; + boost::optional lookahead_fail; std::uint64_t scanned_block_height; std::uint64_t start_height; std::uint64_t blockchain_height; + address_meta lookahead; }; void read_bytes(wire::json_reader&, get_address_txs&); + struct get_version + { + get_version() = delete; + static constexpr const char* endpoint() noexcept { return "/get_version"; } + + std::uint64_t max_subaddresses; + }; + void read_bytes(wire::json_reader&, get_version&); + + struct random_output { random_output() @@ -272,44 +297,36 @@ namespace lwsf { namespace internal { namespace rpc struct subaddrs { - constexpr subaddrs() noexcept - : head({0, 0}), key(0) + subaddrs() noexcept + : value() {} - constexpr explicit subaddrs(const std::uint32_t key) noexcept - : head({0, 0}), key(key) - {} + explicit subaddrs(const std::uint32_t last) noexcept + : value() + { + value.insert({0, last}); + } - std::array head; //!< Only the first element of array - std::uint32_t key; + //! \return true if `this` represents a canonical range of values. + bool is_valid() const noexcept; + + // Merge `{0, index}` to the set of subaddresses. + void merge(std::uint32_t index); + + boost::container::flat_set, std::less<>> value; }; WIRE_JSON_DECLARE_OBJECT(subaddrs); void read_bytes(wire::json_reader&, subaddrs&); - constexpr inline bool operator<(const subaddrs& lhs, const subaddrs& rhs) noexcept - { return lhs.key < rhs.key; } - - template - constexpr inline bool operator<(const subaddrs& lhs, const T rhs) noexcept - { - static_assert(std::is_unsigned()); - return lhs.key < rhs; - } - - template - inline bool operator<(const T lhs, const subaddrs& rhs) noexcept - { - static_assert(std::is_unsigned()); - return lhs < rhs.key; - } - struct get_subaddrs { + using mapped_type = std::pair; get_subaddrs() = delete; static constexpr const char* endpoint() noexcept { return "/get_subaddrs"; } - boost::container::flat_set> all_subaddrs; + boost::container::flat_map all_subaddrs; }; + WIRE_JSON_DECLARE_OBJECT(get_subaddrs::mapped_type); void read_bytes(wire::json_reader&, get_subaddrs&); @@ -383,6 +400,7 @@ namespace lwsf { namespace internal { namespace rpc import_request() = delete; login creds; std::uint64_t from_height; + address_meta lookahead; }; void write_bytes(wire::json_writer&, const import_request&); @@ -396,6 +414,7 @@ namespace lwsf { namespace internal { namespace rpc boost::optional payment_address; boost::optional payment_id; boost::optional import_fee; + boost::optional lookahead; std::string status; bool new_request; bool request_fulfilled; @@ -420,8 +439,8 @@ namespace lwsf { namespace internal { namespace rpc provision_subaddrs_response() = delete; static constexpr const char* endpoint() noexcept { return "/provision_subaddrs"; } - boost::container::flat_set new_subaddrs; - boost::container::flat_set all_subaddrs; + boost::container::flat_map new_subaddrs; + boost::container::flat_map all_subaddrs; }; void read_bytes(wire::json_reader&, provision_subaddrs_response&); @@ -447,7 +466,7 @@ namespace lwsf { namespace internal { namespace rpc { upsert_subaddrs_request() = delete; login creds; - boost::container::flat_set> subaddrs_; + boost::container::flat_map subaddrs_; bool get_all; }; void write_bytes(wire::json_writer&, const upsert_subaddrs_request&); @@ -456,9 +475,6 @@ namespace lwsf { namespace internal { namespace rpc { upsert_subaddrs_response() = delete; static constexpr const char* endpoint() noexcept { return "/upsert_subaddrs"; } - - boost::container::flat_set new_subaddrs; - boost::container::flat_set> all_subaddrs; }; void read_bytes(wire::json_reader&, upsert_subaddrs_response&); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..e69c338 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,29 @@ +# Copyright (c) 2025, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +add_subdirectory(unit) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 0000000..8db6f58 --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright (c) 2025, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +add_library(lwsf-unit-framework framework.test.cpp) +target_include_directories(lwsf-unit-framework PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(lwsf-unit-framework) + +add_executable(lwsf-unit main.cpp backend.test.cpp rpc.test.cpp) + +target_include_directories(lwsf-unit PRIVATE "${lwsf_SOURCE_DIR}/src") +target_link_libraries(lwsf-unit PRIVATE lwsf-api lwsf-unit-framework monero::libraries) +add_test(NAME lwsf-unit COMMAND lwsf-unit -v) diff --git a/tests/unit/backend.test.cpp b/tests/unit/backend.test.cpp new file mode 100644 index 0000000..ef0698d --- /dev/null +++ b/tests/unit/backend.test.cpp @@ -0,0 +1,73 @@ +// Copyright (c) 2025, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "framework.test.h" +#include "backend.h" + +LWS_CASE("backend::acount") +{ + using subaddrs = lwsf::internal::rpc::subaddrs; + + SETUP("empty with zero lookahead") + { + lwsf::internal::backend::account account{.lookahead = {}}; + account.subaccounts.emplace_back().detail.try_emplace(0); + + boost::container::flat_map majors{}; + EXPECT(account.needed_subaddresses(majors) == 0); + + SECTION("standard lookahead") + { + account.lookahead = {50, 200}; + EXPECT(account.needed_subaddresses(majors) == 10000); + + account.subaccounts.back().last = 10; + EXPECT(account.needed_subaddresses(majors) == 10010); + + account.subaccounts.emplace_back().last = 10; + EXPECT(account.needed_subaddresses(majors) == 10220); + } + + SECTION("standard lookahead with overlapping custom lookaheads") + { + account.lookahead = {50, 200}; + majors.try_emplace(0, subaddrs{}).first->second.value = {{10, 20}, {50, 155}}; + + EXPECT(account.needed_subaddresses(majors) == 10000); + } + + SECTION("standard lookahead with non-overlapping custom lookaheads") + { + account.lookahead = {50, 200}; + majors.try_emplace(0, subaddrs{}).first->second.value = {{199, 201}}; + majors.try_emplace(50, subaddrs{}).first->second.value = {{199, 201}}; + + EXPECT(account.needed_subaddresses(majors) == 10005); + } + } +} + diff --git a/tests/unit/framework.test.cpp b/tests/unit/framework.test.cpp new file mode 100644 index 0000000..9bcf8f6 --- /dev/null +++ b/tests/unit/framework.test.cpp @@ -0,0 +1,37 @@ +// Copyright (c) 2025, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "framework.test.h" + +namespace lwsf_test +{ + lest::tests& get_tests() + { + static lest::tests instance; + return instance; + } +} diff --git a/tests/unit/framework.test.h b/tests/unit/framework.test.h new file mode 100644 index 0000000..2d6f215 --- /dev/null +++ b/tests/unit/framework.test.h @@ -0,0 +1,39 @@ +// Copyright (c) 2025, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#define lest_FEATURE_AUTO_REGISTER 1 +#include "lest.hpp" + +#define LWS_CASE(name) \ + lest_CASE(lwsf_test::get_tests(), name) + +namespace lwsf_test +{ + lest::tests& get_tests(); +} diff --git a/tests/unit/lest.hpp b/tests/unit/lest.hpp new file mode 100644 index 0000000..ad09806 --- /dev/null +++ b/tests/unit/lest.hpp @@ -0,0 +1,1487 @@ +// Copyright 2013-2018 by Martin Moene +// +// lest is based on ideas by Kevlin Henney, see video at +// http://skillsmatter.com/podcast/agile-testing/kevlin-henney-rethinking-unit-testing-in-c-plus-plus +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef LEST_LEST_HPP_INCLUDED +#define LEST_LEST_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define lest_MAJOR 1 +#define lest_MINOR 35 +#define lest_PATCH 1 + +#define lest_VERSION lest_STRINGIFY(lest_MAJOR) "." lest_STRINGIFY(lest_MINOR) "." lest_STRINGIFY(lest_PATCH) + +#ifndef lest_FEATURE_AUTO_REGISTER +# define lest_FEATURE_AUTO_REGISTER 0 +#endif + +#ifndef lest_FEATURE_COLOURISE +# define lest_FEATURE_COLOURISE 0 +#endif + +#ifndef lest_FEATURE_LITERAL_SUFFIX +# define lest_FEATURE_LITERAL_SUFFIX 0 +#endif + +#ifndef lest_FEATURE_REGEX_SEARCH +# define lest_FEATURE_REGEX_SEARCH 0 +#endif + +#ifndef lest_FEATURE_TIME_PRECISION +# define lest_FEATURE_TIME_PRECISION 0 +#endif + +#ifndef lest_FEATURE_WSTRING +# define lest_FEATURE_WSTRING 1 +#endif + +#ifdef lest_FEATURE_RTTI +# define lest__cpp_rtti lest_FEATURE_RTTI +#elif defined(__cpp_rtti) +# define lest__cpp_rtti __cpp_rtti +#elif defined(__GXX_RTTI) || defined (_CPPRTTI) +# define lest__cpp_rtti 1 +#else +# define lest__cpp_rtti 0 +#endif + +#if lest_FEATURE_REGEX_SEARCH +# include +#endif + +// Stringify: + +#define lest_STRINGIFY( x ) lest_STRINGIFY_( x ) +#define lest_STRINGIFY_( x ) #x + +// Compiler warning suppression: + +#if defined (__clang__) +# pragma clang diagnostic ignored "-Waggregate-return" +# pragma clang diagnostic ignored "-Woverloaded-shift-op-parentheses" +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-comparison" +#elif defined (__GNUC__) +# pragma GCC diagnostic ignored "-Waggregate-return" +# pragma GCC diagnostic push +#endif + +// Suppress shadow and unused-value warning for sections: + +#if defined (__clang__) +# define lest_SUPPRESS_WSHADOW _Pragma( "clang diagnostic push" ) \ + _Pragma( "clang diagnostic ignored \"-Wshadow\"" ) +# define lest_SUPPRESS_WUNUSED _Pragma( "clang diagnostic push" ) \ + _Pragma( "clang diagnostic ignored \"-Wunused-value\"" ) +# define lest_RESTORE_WARNINGS _Pragma( "clang diagnostic pop" ) + +#elif defined (__GNUC__) +# define lest_SUPPRESS_WSHADOW _Pragma( "GCC diagnostic push" ) \ + _Pragma( "GCC diagnostic ignored \"-Wshadow\"" ) +# define lest_SUPPRESS_WUNUSED _Pragma( "GCC diagnostic push" ) \ + _Pragma( "GCC diagnostic ignored \"-Wunused-value\"" ) +# define lest_RESTORE_WARNINGS _Pragma( "GCC diagnostic pop" ) +#else +# define lest_SUPPRESS_WSHADOW /*empty*/ +# define lest_SUPPRESS_WUNUSED /*empty*/ +# define lest_RESTORE_WARNINGS /*empty*/ +#endif + +// C++ language version detection (C++23 is speculative): +// Note: VC14.0/1900 (VS2015) lacks too much from C++14. + +#ifndef lest_CPLUSPLUS +# if defined(_MSVC_LANG ) && !defined(__clang__) +# define lest_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG ) +# else +# define lest_CPLUSPLUS __cplusplus +# endif +#endif + +#define lest_CPP98_OR_GREATER ( lest_CPLUSPLUS >= 199711L ) +#define lest_CPP11_OR_GREATER ( lest_CPLUSPLUS >= 201103L ) +#define lest_CPP14_OR_GREATER ( lest_CPLUSPLUS >= 201402L ) +#define lest_CPP17_OR_GREATER ( lest_CPLUSPLUS >= 201703L ) +#define lest_CPP20_OR_GREATER ( lest_CPLUSPLUS >= 202002L ) +#define lest_CPP23_OR_GREATER ( lest_CPLUSPLUS >= 202300L ) + +#if ! defined( lest_NO_SHORT_MACRO_NAMES ) && ! defined( lest_NO_SHORT_ASSERTION_NAMES ) +# define MODULE lest_MODULE + +# if ! lest_FEATURE_AUTO_REGISTER +# define CASE lest_CASE +# define CASE_ON lest_CASE_ON +# define SCENARIO lest_SCENARIO +# endif + +# define SETUP lest_SETUP +# define SECTION lest_SECTION + +# define EXPECT lest_EXPECT +# define EXPECT_NOT lest_EXPECT_NOT +# define EXPECT_NO_THROW lest_EXPECT_NO_THROW +# define EXPECT_THROWS lest_EXPECT_THROWS +# define EXPECT_THROWS_AS lest_EXPECT_THROWS_AS + +# define GIVEN lest_GIVEN +# define WHEN lest_WHEN +# define THEN lest_THEN +# define AND_WHEN lest_AND_WHEN +# define AND_THEN lest_AND_THEN +#endif + +#if lest_FEATURE_AUTO_REGISTER +#define lest_SCENARIO( specification, sketch ) lest_CASE( specification, lest::text("Scenario: ") + sketch ) +#else +#define lest_SCENARIO( sketch ) lest_CASE( lest::text("Scenario: ") + sketch ) +#endif +#define lest_GIVEN( context ) lest_SETUP( lest::text(" Given: ") + context ) +#define lest_WHEN( story ) lest_SECTION( lest::text(" When: ") + story ) +#define lest_THEN( story ) lest_SECTION( lest::text(" Then: ") + story ) +#define lest_AND_WHEN( story ) lest_SECTION( lest::text("And then: ") + story ) +#define lest_AND_THEN( story ) lest_SECTION( lest::text("And then: ") + story ) + +#if lest_FEATURE_AUTO_REGISTER + +# define lest_CASE( specification, proposition ) \ + static void lest_FUNCTION( lest::env & ); \ + namespace { lest::add_test lest_REGISTRAR( specification, lest::test( proposition, lest_FUNCTION ) ); } \ + static void lest_FUNCTION( lest::env & lest_env ) + +#else // lest_FEATURE_AUTO_REGISTER + +# define lest_CASE( proposition ) \ + proposition, []( lest::env & lest_env ) + +# define lest_CASE_ON( proposition, ... ) \ + proposition, [__VA_ARGS__]( lest::env & lest_env ) + +# define lest_MODULE( specification, module ) \ + namespace { lest::add_module _( specification, module ); } + +#endif //lest_FEATURE_AUTO_REGISTER + +#define lest_SETUP( context ) \ + for ( int lest__section = 0, lest__count = 1; lest__section < lest__count; lest__count -= 0==lest__section++ ) \ + for ( lest::ctx lest__ctx_setup( lest_env, context ); lest__ctx_setup; ) + +#define lest_SECTION( proposition ) \ + lest_SUPPRESS_WSHADOW \ + static int lest_UNIQUE( id ) = 0; \ + if ( lest::guard( lest_UNIQUE( id ), lest__section, lest__count ) ) \ + for ( int lest__section = 0, lest__count = 1; lest__section < lest__count; lest__count -= 0==lest__section++ ) \ + for ( lest::ctx lest__ctx_section( lest_env, proposition ); lest__ctx_section; ) \ + lest_RESTORE_WARNINGS + +#define lest_EXPECT( expr ) \ + do { \ + try \ + { \ + if ( lest::result score = lest_DECOMPOSE( expr ) ) \ + throw lest::failure{ lest_LOCATION, #expr, score.decomposition }; \ + else if ( lest_env.pass() ) \ + lest::report( lest_env.os, lest::passing{ lest_LOCATION, #expr, score.decomposition, lest_env.zen() }, lest_env.context() ); \ + } \ + catch(...) \ + { \ + lest::inform( lest_LOCATION, #expr ); \ + } \ + } while ( lest::is_false() ) + +#define lest_EXPECT_NOT( expr ) \ + do { \ + try \ + { \ + if ( lest::result score = lest_DECOMPOSE( expr ) ) \ + { \ + if ( lest_env.pass() ) \ + lest::report( lest_env.os, lest::passing{ lest_LOCATION, lest::not_expr( #expr ), lest::not_expr( score.decomposition ), lest_env.zen() }, lest_env.context() ); \ + } \ + else \ + throw lest::failure{ lest_LOCATION, lest::not_expr( #expr ), lest::not_expr( score.decomposition ) }; \ + } \ + catch(...) \ + { \ + lest::inform( lest_LOCATION, lest::not_expr( #expr ) ); \ + } \ + } while ( lest::is_false() ) + +#define lest_EXPECT_NO_THROW( expr ) \ + do \ + { \ + try \ + { \ + lest_SUPPRESS_WUNUSED \ + expr; \ + lest_RESTORE_WARNINGS \ + } \ + catch (...) \ + { \ + lest::inform( lest_LOCATION, #expr ); \ + } \ + if ( lest_env.pass() ) \ + lest::report( lest_env.os, lest::got_none( lest_LOCATION, #expr ), lest_env.context() ); \ + } while ( lest::is_false() ) + +#define lest_EXPECT_THROWS( expr ) \ + do \ + { \ + try \ + { \ + lest_SUPPRESS_WUNUSED \ + expr; \ + lest_RESTORE_WARNINGS \ + } \ + catch (...) \ + { \ + if ( lest_env.pass() ) \ + lest::report( lest_env.os, lest::got{ lest_LOCATION, #expr }, lest_env.context() ); \ + break; \ + } \ + throw lest::expected{ lest_LOCATION, #expr }; \ + } \ + while ( lest::is_false() ) + +#define lest_EXPECT_THROWS_AS( expr, excpt ) \ + do \ + { \ + try \ + { \ + lest_SUPPRESS_WUNUSED \ + expr; \ + lest_RESTORE_WARNINGS \ + } \ + catch ( excpt & ) \ + { \ + if ( lest_env.pass() ) \ + lest::report( lest_env.os, lest::got{ lest_LOCATION, #expr, lest::of_type( #excpt ) }, lest_env.context() ); \ + break; \ + } \ + catch (...) {} \ + throw lest::expected{ lest_LOCATION, #expr, lest::of_type( #excpt ) }; \ + } \ + while ( lest::is_false() ) + +#define lest_UNIQUE( name ) lest_UNIQUE2( name, __LINE__ ) +#define lest_UNIQUE2( name, line ) lest_UNIQUE3( name, line ) +#define lest_UNIQUE3( name, line ) name ## line + +#define lest_DECOMPOSE( expr ) ( lest::expression_decomposer() << expr ) + +#define lest_FUNCTION lest_UNIQUE(__lest_function__ ) +#define lest_REGISTRAR lest_UNIQUE(__lest_registrar__ ) + +#define lest_LOCATION lest::location{__FILE__, __LINE__} + +namespace lest { + +const int exit_max_value = 255; + +using text = std::string; +using texts = std::vector; + +struct env; + +struct test +{ + text name; + std::function behaviour; + +#if lest_FEATURE_AUTO_REGISTER + test( text name_, std::function behaviour_ ) + : name( name_), behaviour( behaviour_) {} +#endif +}; + +using tests = std::vector; + +#if lest_FEATURE_AUTO_REGISTER + +struct add_test +{ + add_test( tests & specification, test const & test_case ) + { + specification.push_back( test_case ); + } +}; + +#else + +struct add_module +{ + template< std::size_t N > + add_module( tests & specification, test const (&module)[N] ) + { + specification.insert( specification.end(), std::begin( module ), std::end( module ) ); + } +}; + +#endif + +struct result +{ + const bool passed; + const text decomposition; + + template< typename T > + result( T const & passed_, text decomposition_) + : passed( !!passed_), decomposition( decomposition_) {} + + explicit operator bool() { return ! passed; } +}; + +struct location +{ + const text file; + const int line; + + location( text file_, int line_) + : file( file_), line( line_) {} +}; + +struct comment +{ + const text info; + + comment( text info_) : info( info_) {} + explicit operator bool() { return ! info.empty(); } +}; + +struct message : std::runtime_error +{ + const text kind; + const location where; + const comment note; + + ~message() throw() {} // GCC 4.6 + + message( text kind_, location where_, text expr_, text note_ = "" ) + : std::runtime_error( expr_), kind( kind_), where( where_), note( note_) {} +}; + +struct failure : message +{ + failure( location where_, text expr_, text decomposition_) + : message{ "failed", where_, expr_ + " for " + decomposition_ } {} +}; + +struct success : message +{ +// using message::message; // VC is lagging here + + success( text kind_, location where_, text expr_, text note_ = "" ) + : message( kind_, where_, expr_, note_ ) {} +}; + +struct passing : success +{ + passing( location where_, text expr_, text decomposition_, bool zen ) + : success( "passed", where_, expr_ + (zen ? "":" for " + decomposition_) ) {} +}; + +struct got_none : success +{ + got_none( location where_, text expr_ ) + : success( "passed: got no exception", where_, expr_ ) {} +}; + +struct got : success +{ + got( location where_, text expr_) + : success( "passed: got exception", where_, expr_) {} + + got( location where_, text expr_, text excpt_) + : success( "passed: got exception " + excpt_, where_, expr_) {} +}; + +struct expected : message +{ + expected( location where_, text expr_, text excpt_ = "" ) + : message{ "failed: didn't get exception", where_, expr_, excpt_ } {} +}; + +struct unexpected : message +{ + unexpected( location where_, text expr_, text note_ = "" ) + : message{ "failed: got unexpected exception", where_, expr_, note_ } {} +}; + +struct guard +{ + int & id; + int const & section; + + guard( int & id_, int const & section_, int & count ) + : id( id_), section( section_) + { + if ( section == 0 ) + id = count++ - 1; + } + operator bool() { return id == section; } +}; + +class approx +{ +public: + explicit approx ( double magnitude ) + : epsilon_ { std::numeric_limits::epsilon() * 100 } + , scale_ { 1.0 } + , magnitude_{ magnitude } {} + + approx( approx const & other ) = default; + + static approx custom() { return approx( 0 ); } + + approx operator()( double new_magnitude ) + { + approx appr( new_magnitude ); + appr.epsilon( epsilon_ ); + appr.scale ( scale_ ); + return appr; + } + + double magnitude() const { return magnitude_; } + + approx & epsilon( double epsilon ) { epsilon_ = epsilon; return *this; } + approx & scale ( double scale ) { scale_ = scale; return *this; } + + friend bool operator == ( double lhs, approx const & rhs ) + { + // Thanks to Richard Harris for his help refining this formula. + return std::abs( lhs - rhs.magnitude_ ) < rhs.epsilon_ * ( rhs.scale_ + (std::min)( std::abs( lhs ), std::abs( rhs.magnitude_ ) ) ); + } + + friend bool operator == ( approx const & lhs, double rhs ) { return operator==( rhs, lhs ); } + friend bool operator != ( double lhs, approx const & rhs ) { return !operator==( lhs, rhs ); } + friend bool operator != ( approx const & lhs, double rhs ) { return !operator==( rhs, lhs ); } + + friend bool operator <= ( double lhs, approx const & rhs ) { return lhs < rhs.magnitude_ || lhs == rhs; } + friend bool operator <= ( approx const & lhs, double rhs ) { return lhs.magnitude_ < rhs || lhs == rhs; } + friend bool operator >= ( double lhs, approx const & rhs ) { return lhs > rhs.magnitude_ || lhs == rhs; } + friend bool operator >= ( approx const & lhs, double rhs ) { return lhs.magnitude_ > rhs || lhs == rhs; } + +private: + double epsilon_; + double scale_; + double magnitude_; +}; + +inline bool is_false( ) { return false; } +inline bool is_true ( bool flag ) { return flag; } + +inline text not_expr( text message ) +{ + return "! ( " + message + " )"; +} + +inline text with_message( text message ) +{ + return "with message \"" + message + "\""; +} + +inline text of_type( text type ) +{ + return "of type " + type; +} + +inline void inform( location where, text expr ) +{ + try + { + throw; + } + catch( message const & ) + { + throw; + } + catch( std::exception const & e ) + { + throw unexpected{ where, expr, with_message( e.what() ) }; \ + } + catch(...) + { + throw unexpected{ where, expr, "of unknown type" }; \ + } +} + +// Expression decomposition: + +template< typename T > +auto make_value_string( T const & value ) -> std::string; + +template< typename T > +auto make_memory_string( T const & item ) -> std::string; + +#if lest_FEATURE_LITERAL_SUFFIX +inline char const * sfx( char const * txt ) { return txt; } +#else +inline char const * sfx( char const * ) { return ""; } +#endif + +inline std::string transformed( char chr ) +{ + struct Tr { char chr; char const * str; } table[] = + { + {'\\', "\\\\" }, + {'\r', "\\r" }, {'\f', "\\f" }, + {'\n', "\\n" }, {'\t', "\\t" }, + }; + + for ( auto tr : table ) + { + if ( chr == tr.chr ) + return tr.str; + } + + auto unprintable = [](char c){ return 0 <= c && c < ' '; }; + + auto to_hex_string = [](char c) + { + std::ostringstream os; + os << "\\x" << std::hex << std::setw(2) << std::setfill('0') << static_cast( static_cast(c) ); + return os.str(); + }; + + return unprintable( chr ) ? to_hex_string( chr ) : std::string( 1, chr ); +} + +inline std::string make_tran_string( std::string const & txt ) { std::ostringstream os; for(auto c:txt) os << transformed(c); return os.str(); } +inline std::string make_strg_string( std::string const & txt ) { return "\"" + make_tran_string( txt ) + "\"" ; } +inline std::string make_char_string( char chr ) { return "\'" + make_tran_string( std::string( 1, chr ) ) + "\'" ; } + +inline std::string to_string( std::nullptr_t ) { return "nullptr"; } +inline std::string to_string( std::string const & txt ) { return make_strg_string( txt ); } +#if lest_FEATURE_WSTRING +inline std::string to_string( std::wstring const & txt ) ; +#endif + +inline std::string to_string( char const * const txt ) { return txt ? make_strg_string( txt ) : "{null string}"; } +inline std::string to_string( char * const txt ) { return txt ? make_strg_string( txt ) : "{null string}"; } +#if lest_FEATURE_WSTRING +inline std::string to_string( wchar_t const * const txt ) { return txt ? to_string( std::wstring( txt ) ) : "{null string}"; } +inline std::string to_string( wchar_t * const txt ) { return txt ? to_string( std::wstring( txt ) ) : "{null string}"; } +#endif + +inline std::string to_string( bool flag ) { return flag ? "true" : "false"; } + +inline std::string to_string( signed short value ) { return make_value_string( value ) ; } +inline std::string to_string( unsigned short value ) { return make_value_string( value ) + sfx("u" ); } +inline std::string to_string( signed int value ) { return make_value_string( value ) ; } +inline std::string to_string( unsigned int value ) { return make_value_string( value ) + sfx("u" ); } +inline std::string to_string( signed long value ) { return make_value_string( value ) + sfx("l" ); } +inline std::string to_string( unsigned long value ) { return make_value_string( value ) + sfx("ul" ); } +inline std::string to_string( signed long long value ) { return make_value_string( value ) + sfx("ll" ); } +inline std::string to_string( unsigned long long value ) { return make_value_string( value ) + sfx("ull"); } +inline std::string to_string( double value ) { return make_value_string( value ) ; } +inline std::string to_string( float value ) { return make_value_string( value ) + sfx("f" ); } + +inline std::string to_string( signed char chr ) { return make_char_string( static_cast( chr ) ); } +inline std::string to_string( unsigned char chr ) { return make_char_string( static_cast( chr ) ); } +inline std::string to_string( char chr ) { return make_char_string( chr ); } + +template< typename T > +struct is_streamable +{ + template< typename U > + static auto test( int ) -> decltype( std::declval() << std::declval(), std::true_type() ); + + template< typename > + static auto test( ... ) -> std::false_type; + +#ifdef _MSC_VER + enum { value = std::is_same< decltype( test(0) ), std::true_type >::value }; +#else + static constexpr bool value = std::is_same< decltype( test(0) ), std::true_type >::value; +#endif +}; + +template< typename T > +struct is_container +{ + template< typename U > + static auto test( int ) -> decltype( std::declval().begin() == std::declval().end(), std::true_type() ); + + template< typename > + static auto test( ... ) -> std::false_type; + +#ifdef _MSC_VER + enum { value = std::is_same< decltype( test(0) ), std::true_type >::value }; +#else + static constexpr bool value = std::is_same< decltype( test(0) ), std::true_type >::value; +#endif +}; + +template< typename T, typename R > +using ForEnum = typename std::enable_if< std::is_enum::value, R>::type; + +template< typename T, typename R > +using ForNonEnum = typename std::enable_if< ! std::is_enum::value, R>::type; + +template< typename T, typename R > +using ForStreamable = typename std::enable_if< is_streamable::value, R>::type; + +template< typename T, typename R > +using ForNonStreamable = typename std::enable_if< ! is_streamable::value, R>::type; + +template< typename T, typename R > +using ForContainer = typename std::enable_if< is_container::value, R>::type; + +template< typename T, typename R > +using ForNonContainerNonPointer = typename std::enable_if< ! (is_container::value || std::is_pointer::value), R>::type; + +template< typename T > +auto to_string( T const & item ) -> ForNonContainerNonPointer; + +template< typename T > +auto make_enum_string( T const & item ) -> ForNonEnum +{ +#if lest__cpp_rtti + return text("[type: ") + typeid(T).name() + "]: " + make_memory_string( item ); +#else + return text("[type: (no RTTI)]: ") + make_memory_string( item ); +#endif +} + +template< typename T > +auto make_enum_string( T const & item ) -> ForEnum +{ + return to_string( static_cast::type>( item ) ); +} + +template< typename T > +auto make_string( T const & item ) -> ForNonStreamable +{ + return make_enum_string( item ); +} + +template< typename T > +auto make_string( T const & item ) -> ForStreamable +{ + std::ostringstream os; os << item; return os.str(); +} + +template +auto make_string( std::pair const & pair ) -> std::string +{ + std::ostringstream oss; + oss << "{ " << to_string( pair.first ) << ", " << to_string( pair.second ) << " }"; + return oss.str(); +} + +template< typename TU, std::size_t N > +struct make_tuple_string +{ + static std::string make( TU const & tuple ) + { + std::ostringstream os; + os << to_string( std::get( tuple ) ) << ( N < std::tuple_size::value ? ", ": " "); + return make_tuple_string::make( tuple ) + os.str(); + } +}; + +template< typename TU > +struct make_tuple_string +{ + static std::string make( TU const & ) { return ""; } +}; + +template< typename ...TS > +auto make_string( std::tuple const & tuple ) -> std::string +{ + return "{ " + make_tuple_string, sizeof...(TS)>::make( tuple ) + "}"; +} + +template< typename T > +inline std::string make_string( T const * ptr ) +{ + // Note showbase affects the behavior of /integer/ output; + std::ostringstream os; + os << std::internal << std::hex << std::showbase << std::setw( 2 + 2 * sizeof(T*) ) << std::setfill('0') << reinterpret_cast( ptr ); + return os.str(); +} + +template< typename C, typename R > +inline std::string make_string( R C::* ptr ) +{ + std::ostringstream os; + os << std::internal << std::hex << std::showbase << std::setw( 2 + 2 * sizeof(R C::* ) ) << std::setfill('0') << ptr; + return os.str(); +} + +template< typename T > +auto to_string( T const * ptr ) -> std::string +{ + return ! ptr ? "nullptr" : make_string( ptr ); +} + +template +auto to_string( R C::* ptr ) -> std::string +{ + return ! ptr ? "nullptr" : make_string( ptr ); +} + +template< typename T > +auto to_string( T const & item ) -> ForNonContainerNonPointer +{ + return make_string( item ); +} + +template< typename C > +auto to_string( C const & cont ) -> ForContainer +{ + std::ostringstream os; + os << "{ "; + for ( auto & x : cont ) + { + os << to_string( x ) << ", "; + } + os << "}"; + return os.str(); +} + +#if lest_FEATURE_WSTRING +inline +auto to_string( std::wstring const & txt ) -> std::string +{ + std::string result; result.reserve( txt.size() ); + + for( auto & chr : txt ) + { + result += chr <= 0xff ? static_cast( chr ) : '?'; + } + return to_string( result ); +} +#endif + +template< typename T > +auto make_value_string( T const & value ) -> std::string +{ + std::ostringstream os; os << value; return os.str(); +} + +inline +auto make_memory_string( void const * item, std::size_t size ) -> std::string +{ + // reverse order for little endian architectures: + + auto is_little_endian = [] + { + union U { int i = 1; char c[ sizeof(int) ]; }; + + return 1 != U{}.c[ sizeof(int) - 1 ]; + }; + + int i = 0, end = static_cast( size ), inc = 1; + + if ( is_little_endian() ) { i = end - 1; end = inc = -1; } + + unsigned char const * bytes = static_cast( item ); + + std::ostringstream os; + os << "0x" << std::setfill( '0' ) << std::hex; + for ( ; i != end; i += inc ) + { + os << std::setw(2) << static_cast( bytes[i] ) << " "; + } + return os.str(); +} + +template< typename T > +auto make_memory_string( T const & item ) -> std::string +{ + return make_memory_string( &item, sizeof item ); +} + +inline +auto to_string( approx const & appr ) -> std::string +{ + return to_string( appr.magnitude() ); +} + +template< typename L, typename R > +auto to_string( L const & lhs, std::string op, R const & rhs ) -> std::string +{ + std::ostringstream os; os << to_string( lhs ) << " " << op << " " << to_string( rhs ); return os.str(); +} + +template< typename L > +struct expression_lhs +{ + const L lhs; + + expression_lhs( L lhs_) : lhs( lhs_) {} + + operator result() { return result{ !!lhs, to_string( lhs ) }; } + + template< typename R > result operator==( R const & rhs ) { return result{ lhs == rhs, to_string( lhs, "==", rhs ) }; } + template< typename R > result operator!=( R const & rhs ) { return result{ lhs != rhs, to_string( lhs, "!=", rhs ) }; } + template< typename R > result operator< ( R const & rhs ) { return result{ lhs < rhs, to_string( lhs, "<" , rhs ) }; } + template< typename R > result operator<=( R const & rhs ) { return result{ lhs <= rhs, to_string( lhs, "<=", rhs ) }; } + template< typename R > result operator> ( R const & rhs ) { return result{ lhs > rhs, to_string( lhs, ">" , rhs ) }; } + template< typename R > result operator>=( R const & rhs ) { return result{ lhs >= rhs, to_string( lhs, ">=", rhs ) }; } +}; + +struct expression_decomposer +{ + template + expression_lhs operator<< ( L const & operand ) + { + return expression_lhs( operand ); + } +}; + +// Reporter: + +#if lest_FEATURE_COLOURISE + +inline text red ( text words ) { return "\033[1;31m" + words + "\033[0m"; } +inline text green( text words ) { return "\033[1;32m" + words + "\033[0m"; } +inline text gray ( text words ) { return "\033[1;30m" + words + "\033[0m"; } + +inline bool starts_with( text words, text with ) +{ + return 0 == words.find( with ); +} + +inline text replace( text words, text from, text to ) +{ + size_t pos = words.find( from ); + return pos == std::string::npos ? words : words.replace( pos, from.length(), to ); +} + +inline text colour( text words ) +{ + if ( starts_with( words, "failed" ) ) return replace( words, "failed", red ( "failed" ) ); + else if ( starts_with( words, "passed" ) ) return replace( words, "passed", green( "passed" ) ); + + return replace( words, "for", gray( "for" ) ); +} + +inline bool is_cout( std::ostream & os ) { return &os == &std::cout; } + +struct colourise +{ + const text words; + + colourise( text words ) + : words( words ) {} + + // only colourise for std::cout, not for a stringstream as used in tests: + + std::ostream & operator()( std::ostream & os ) const + { + return is_cout( os ) ? os << colour( words ) : os << words; + } +}; + +inline std::ostream & operator<<( std::ostream & os, colourise words ) { return words( os ); } +#else +inline text colourise( text words ) { return words; } +#endif + +inline text pluralise( text word, int n ) +{ + return n == 1 ? word : word + "s"; +} + +inline std::ostream & operator<<( std::ostream & os, comment note ) +{ + return os << (note ? " " + note.info : "" ); +} + +inline std::ostream & operator<<( std::ostream & os, location where ) +{ +#ifdef __GNUG__ + return os << where.file << ":" << where.line; +#else + return os << where.file << "(" << where.line << ")"; +#endif +} + +inline void report( std::ostream & os, message const & e, text test ) +{ + os << e.where << ": " << colourise( e.kind ) << e.note << ": " << test << ": " << colourise( e.what() ) << std::endl; +} + +// Test runner: + +#if lest_FEATURE_REGEX_SEARCH + inline bool search( text re, text line ) + { + return std::regex_search( line, std::regex( re ) ); + } +#else + inline bool search( text part, text line ) + { + auto case_insensitive_equal = []( char a, char b ) + { + return tolower( a ) == tolower( b ); + }; + + return std::search( + line.begin(), line.end(), + part.begin(), part.end(), case_insensitive_equal ) != line.end(); + } +#endif + +inline bool match( texts whats, text line ) +{ + for ( auto & what : whats ) + { + if ( search( what, line ) ) + return true; + } + return false; +} + +inline bool select( text name, texts include ) +{ + auto none = []( texts args ) { return args.size() == 0; }; + +#if lest_FEATURE_REGEX_SEARCH + auto hidden = []( text arg ){ return match( { "\\[\\..*", "\\[hide\\]" }, arg ); }; +#else + auto hidden = []( text arg ){ return match( { "[.", "[hide]" }, arg ); }; +#endif + + if ( none( include ) ) + { + return ! hidden( name ); + } + + bool any = false; + for ( auto pos = include.rbegin(); pos != include.rend(); ++pos ) + { + auto & part = *pos; + + if ( part == "@" || part == "*" ) + return true; + + if ( search( part, name ) ) + return true; + + if ( '!' == part[0] ) + { + any = true; + if ( search( part.substr(1), name ) ) + return false; + } + else + { + any = false; + } + } + return any && ! hidden( name ); +} + +inline int indefinite( int repeat ) { return repeat == -1; } + +using seed_t = std::mt19937::result_type; + +struct options +{ + bool help = false; + bool abort = false; + bool count = false; + bool list = false; + bool tags = false; + bool time = false; + bool pass = false; + bool zen = false; + bool lexical = false; + bool random = false; + bool verbose = false; + bool version = false; + int repeat = 1; + seed_t seed = 0; +}; + +struct env +{ + std::ostream & os; + options opt; + text testing; + std::vector< text > ctx; + + env( std::ostream & out, options option ) + : os( out ), opt( option ), testing(), ctx() {} + + env & operator()( text test ) + { + clear(); testing = test; return *this; + } + + bool abort() { return opt.abort; } + bool pass() { return opt.pass; } + bool zen() { return opt.zen; } + + void clear() { ctx.clear(); } + void pop() { ctx.pop_back(); } + void push( text proposition ) { ctx.emplace_back( proposition ); } + + text context() { return testing + sections(); } + + text sections() + { + if ( ! opt.verbose ) + return ""; + + text msg; + for( auto section : ctx ) + { + msg += "\n " + section; + } + return msg; + } +}; + +struct ctx +{ + env & environment; + bool once; + + ctx( env & environment_, text proposition_ ) + : environment( environment_), once( true ) + { + environment.push( proposition_); + } + + ~ctx() + { +#if lest_CPP17_OR_GREATER + if ( std::uncaught_exceptions() == 0 ) +#else + if ( ! std::uncaught_exception() ) +#endif + { + environment.pop(); + } + } + + explicit operator bool() { bool result = once; once = false; return result; } +}; + +struct action +{ + std::ostream & os; + + action( std::ostream & out ) : os( out ) {} + + action( action const & ) = delete; + void operator=( action const & ) = delete; + + operator int() { return 0; } + bool abort() { return false; } + action & operator()( test ) { return *this; } +}; + +struct print : action +{ + print( std::ostream & out ) : action( out ) {} + + print & operator()( test testing ) + { + os << testing.name << "\n"; return *this; + } +}; + +inline texts tags( text name, texts result = {} ) +{ + auto none = std::string::npos; + auto lb = name.find_first_of( "[" ); + auto rb = name.find_first_of( "]" ); + + if ( lb == none || rb == none ) + return result; + + result.emplace_back( name.substr( lb, rb - lb + 1 ) ); + + return tags( name.substr( rb + 1 ), result ); +} + +struct ptags : action +{ + std::set result; + + ptags( std::ostream & out ) : action( out ), result() {} + + ptags & operator()( test testing ) + { + for ( auto & tag : tags( testing.name ) ) + result.insert( tag ); + + return *this; + } + + ~ptags() + { + std::copy( result.begin(), result.end(), std::ostream_iterator( os, "\n" ) ); + } +}; + +struct count : action +{ + int n = 0; + + count( std::ostream & out ) : action( out ) {} + + count & operator()( test ) { ++n; return *this; } + + ~count() + { + os << n << " selected " << pluralise("test", n) << "\n"; + } +}; + +struct timer +{ + using time = std::chrono::high_resolution_clock; + + time::time_point start = time::now(); + + double elapsed_seconds() const + { + return 1e-6 * static_cast( std::chrono::duration_cast< std::chrono::microseconds >( time::now() - start ).count() ); + } +}; + +struct times : action +{ + env output; + int selected = 0; + int failures = 0; + + timer total; + + times( std::ostream & out, options option ) + : action( out ), output( out, option ), total() + { + os << std::setfill(' ') << std::fixed << std::setprecision( lest_FEATURE_TIME_PRECISION ); + } + + operator int() { return failures; } + + bool abort() { return output.abort() && failures > 0; } + + times & operator()( test testing ) + { + timer t; + + try + { + testing.behaviour( output( testing.name ) ); + } + catch( message const & ) + { + ++failures; + } + + os << std::setw(3) << ( 1000 * t.elapsed_seconds() ) << " ms: " << testing.name << "\n"; + + return *this; + } + + ~times() + { + os << "Elapsed time: " << std::setprecision(1) << total.elapsed_seconds() << " s\n"; + } +}; + +struct confirm : action +{ + env output; + int selected = 0; + int failures = 0; + + confirm( std::ostream & out, options option ) + : action( out ), output( out, option ) {} + + operator int() { return failures; } + + bool abort() { return output.abort() && failures > 0; } + + confirm & operator()( test testing ) + { + try + { + ++selected; testing.behaviour( output( testing.name ) ); + } + catch( message const & e ) + { + ++failures; report( os, e, output.context() ); + } + return *this; + } + + ~confirm() + { + if ( failures > 0 ) + { + os << failures << " out of " << selected << " selected " << pluralise("test", selected) << " " << colourise( "failed.\n" ); + } + else if ( output.pass() ) + { + os << "All " << selected << " selected " << pluralise("test", selected) << " " << colourise( "passed.\n" ); + } + } +}; + +template< typename Action > +bool abort( Action & perform ) +{ + return perform.abort(); +} + +template< typename Action > +Action && for_test( tests specification, texts in, Action && perform, int n = 1 ) +{ + for ( int i = 0; indefinite( n ) || i < n; ++i ) + { + for ( auto & testing : specification ) + { + if ( select( testing.name, in ) ) + if ( abort( perform( testing ) ) ) + return std::move( perform ); + } + } + return std::move( perform ); +} + +inline void sort( tests & specification ) +{ + auto test_less = []( test const & a, test const & b ) { return a.name < b.name; }; + std::sort( specification.begin(), specification.end(), test_less ); +} + +inline void shuffle( tests & specification, options option ) +{ + std::shuffle( specification.begin(), specification.end(), std::mt19937( option.seed ) ); +} + +// workaround MinGW bug, http://stackoverflow.com/a/16132279: + +inline int stoi( text num ) +{ + return static_cast( std::strtol( num.c_str(), nullptr, 10 ) ); +} + +inline bool is_number( text arg ) +{ + return std::all_of( arg.begin(), arg.end(), ::isdigit ); +} + +inline seed_t seed( text opt, text arg ) +{ + if ( is_number( arg ) ) + return static_cast( lest::stoi( arg ) ); + + if ( arg == "time" ) + return static_cast( std::chrono::high_resolution_clock::now().time_since_epoch().count() ); + + throw std::runtime_error( "expecting 'time' or positive number with option '" + opt + "', got '" + arg + "' (try option --help)" ); +} + +inline int repeat( text opt, text arg ) +{ + const int num = lest::stoi( arg ); + + if ( indefinite( num ) || num >= 0 ) + return num; + + throw std::runtime_error( "expecting '-1' or positive number with option '" + opt + "', got '" + arg + "' (try option --help)" ); +} + +inline auto split_option( text arg ) -> std::tuple +{ + auto pos = arg.rfind( '=' ); + + return pos == text::npos + ? std::make_tuple( arg, "" ) + : std::make_tuple( arg.substr( 0, pos ), arg.substr( pos + 1 ) ); +} + +inline auto split_arguments( texts args ) -> std::tuple +{ + options option; texts in; + + bool in_options = true; + + for ( auto & arg : args ) + { + if ( in_options ) + { + text opt, val; + std::tie( opt, val ) = split_option( arg ); + + if ( opt[0] != '-' ) { in_options = false; } + else if ( opt == "--" ) { in_options = false; continue; } + else if ( opt == "-h" || "--help" == opt ) { option.help = true; continue; } + else if ( opt == "-a" || "--abort" == opt ) { option.abort = true; continue; } + else if ( opt == "-c" || "--count" == opt ) { option.count = true; continue; } + else if ( opt == "-g" || "--list-tags" == opt ) { option.tags = true; continue; } + else if ( opt == "-l" || "--list-tests" == opt ) { option.list = true; continue; } + else if ( opt == "-t" || "--time" == opt ) { option.time = true; continue; } + else if ( opt == "-p" || "--pass" == opt ) { option.pass = true; continue; } + else if ( opt == "-z" || "--pass-zen" == opt ) { option.zen = true; continue; } + else if ( opt == "-v" || "--verbose" == opt ) { option.verbose = true; continue; } + else if ( "--version" == opt ) { option.version = true; continue; } + else if ( opt == "--order" && "declared" == val ) { /* by definition */ ; continue; } + else if ( opt == "--order" && "lexical" == val ) { option.lexical = true; continue; } + else if ( opt == "--order" && "random" == val ) { option.random = true; continue; } + else if ( opt == "--random-seed" ) { option.seed = seed ( "--random-seed", val ); continue; } + else if ( opt == "--repeat" ) { option.repeat = repeat( "--repeat" , val ); continue; } + else throw std::runtime_error( "unrecognised option '" + arg + "' (try option --help)" ); + } + in.push_back( arg ); + } + option.pass = option.pass || option.zen; + + return std::make_tuple( option, in ); +} + +inline int usage( std::ostream & os ) +{ + os << + "\nUsage: test [options] [test-spec ...]\n" + "\n" + "Options:\n" + " -h, --help this help message\n" + " -a, --abort abort at first failure\n" + " -c, --count count selected tests\n" + " -g, --list-tags list tags of selected tests\n" + " -l, --list-tests list selected tests\n" + " -p, --pass also report passing tests\n" + " -z, --pass-zen ... without expansion\n" + " -t, --time list duration of selected tests\n" + " -v, --verbose also report passing or failing sections\n" + " --order=declared use source code test order (default)\n" + " --order=lexical use lexical sort test order\n" + " --order=random use random test order\n" + " --random-seed=n use n for random generator seed\n" + " --random-seed=time use time for random generator seed\n" + " --repeat=n repeat selected tests n times (-1: indefinite)\n" + " --version report lest version and compiler used\n" + " -- end options\n" + "\n" + "Test specification:\n" + " \"@\", \"*\" all tests, unless excluded\n" + " empty all tests, unless tagged [hide] or [.optional-name]\n" +#if lest_FEATURE_REGEX_SEARCH + " \"re\" select tests that match regular expression\n" + " \"!re\" omit tests that match regular expression\n" +#else + " \"text\" select tests that contain text (case insensitive)\n" + " \"!text\" omit tests that contain text (case insensitive)\n" +#endif + ; + return 0; +} + +inline text compiler() +{ + std::ostringstream os; +#if defined (__clang__ ) + os << "clang " << __clang_version__; +#elif defined (__GNUC__ ) + os << "gcc " << __GNUC__ << "." << __GNUC_MINOR__ << "." << __GNUC_PATCHLEVEL__; +#elif defined ( _MSC_VER ) + os << "MSVC " << (_MSC_VER / 100 - 5 - (_MSC_VER < 1900)) << " (" << _MSC_VER << ")"; +#else + os << "[compiler]"; +#endif + return os.str(); +} + +inline int version( std::ostream & os ) +{ + os << "lest version " << lest_VERSION << "\n" + << "Compiled with " << compiler() << " on " << __DATE__ << " at " << __TIME__ << ".\n" + << "For more information, see https://github.com/martinmoene/lest.\n"; + return 0; +} + +inline int run( tests specification, texts arguments, std::ostream & os = std::cout ) +{ + try + { + options option; texts in; + std::tie( option, in ) = split_arguments( arguments ); + + if ( option.lexical ) { sort( specification ); } + if ( option.random ) { shuffle( specification, option ); } + + if ( option.help ) { return usage ( os ); } + if ( option.version ) { return version ( os ); } + if ( option.count ) { return for_test( specification, in, count( os ) ); } + if ( option.list ) { return for_test( specification, in, print( os ) ); } + if ( option.tags ) { return for_test( specification, in, ptags( os ) ); } + if ( option.time ) { return for_test( specification, in, times( os, option ) ); } + + return for_test( specification, in, confirm( os, option ), option.repeat ); + } + catch ( std::exception const & e ) + { + os << "Error: " << e.what() << "\n"; + return 1; + } +} + +inline int run( tests specification, int argc, char * argv[], std::ostream & os = std::cout ) +{ + return run( specification, texts( argv + 1, argv + argc ), os ); +} + +template< std::size_t N > +int run( test const (&specification)[N], texts arguments, std::ostream & os = std::cout ) +{ + std::cout.sync_with_stdio( false ); + return (std::min)( run( tests( specification, specification + N ), arguments, os ), exit_max_value ); +} + +template< std::size_t N > +int run( test const (&specification)[N], std::ostream & os = std::cout ) +{ + return run( tests( specification, specification + N ), {}, os ); +} + +template< std::size_t N > +int run( test const (&specification)[N], int argc, char * argv[], std::ostream & os = std::cout ) +{ + return run( tests( specification, specification + N ), texts( argv + 1, argv + argc ), os ); +} + +} // namespace lest + +#if defined (__clang__) +# pragma clang diagnostic pop +#elif defined (__GNUC__) +# pragma GCC diagnostic pop +#endif + +#endif // LEST_LEST_HPP_INCLUDED diff --git a/tests/unit/main.cpp b/tests/unit/main.cpp new file mode 100644 index 0000000..9d69633 --- /dev/null +++ b/tests/unit/main.cpp @@ -0,0 +1,33 @@ +// Copyright (c) 2025, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "framework.test.h" + +int main(int argc, char* argv[]) +{ + return lest::run(lwsf_test::get_tests(), argc, argv); +} diff --git a/tests/unit/rpc.test.cpp b/tests/unit/rpc.test.cpp new file mode 100644 index 0000000..f5a30a6 --- /dev/null +++ b/tests/unit/rpc.test.cpp @@ -0,0 +1,159 @@ +// Copyright (c) 2025, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "framework.test.h" +#include "lwsf_rpc.h" + +LWS_CASE("rpc::subaddrs") +{ + SETUP("empty subaddrs") + { + lwsf::internal::rpc::subaddrs subaddr{}; + EXPECT(subaddr.value.empty()); + EXPECT(subaddr.is_valid()); + + SECTION("constructor") + { + subaddr = lwsf::internal::rpc::subaddrs{10}; + EXPECT(subaddr.is_valid()); + EXPECT(subaddr.value.size() == 1); + EXPECT(std::get<0>(*subaddr.value.nth(0)) == 0); + EXPECT(std::get<1>(*subaddr.value.nth(0)) == 10); + } + + SECTION("is_valid") + { + subaddr.value = {{0, 0}}; + EXPECT(subaddr.is_valid()); + + subaddr.value = {{0, 1}, {2, 3}}; + EXPECT(subaddr.is_valid()); + + subaddr.value = {{1, 0}}; + EXPECT(!subaddr.is_valid()); + + subaddr.value = {{0, 1}, {1, 3}}; + EXPECT(!subaddr.is_valid()); + } + + SECTION("merge from empty") + { + subaddr.merge(100); + EXPECT(subaddr.is_valid()); + EXPECT(subaddr.value.size() == 1); + EXPECT(std::get<0>(*subaddr.value.nth(0)) == 0); + EXPECT(std::get<1>(*subaddr.value.nth(0)) == 100); + } + + SECTION("merge from one item prepend") + { + subaddr.value = {{10, 12}}; + subaddr.merge(9); + EXPECT(subaddr.is_valid()); + EXPECT(subaddr.value.size() == 2); + EXPECT(std::get<0>(*subaddr.value.nth(0)) == 0); + EXPECT(std::get<1>(*subaddr.value.nth(0)) == 9); + EXPECT(std::get<0>(*subaddr.value.nth(1)) == 10); + EXPECT(std::get<1>(*subaddr.value.nth(1)) == 12); + } + + SECTION("merge from one item front") + { + subaddr.value = {{10, 12}}; + subaddr.merge(10); + EXPECT(subaddr.is_valid()); + EXPECT(subaddr.value.size() == 1); + EXPECT(std::get<0>(*subaddr.value.nth(0)) == 0); + EXPECT(std::get<1>(*subaddr.value.nth(0)) == 12); + } + + SECTION("merge from one item middle") + { + subaddr.value = {{10, 12}}; + subaddr.merge(11); + EXPECT(subaddr.is_valid()); + EXPECT(subaddr.value.size() == 1); + EXPECT(std::get<0>(*subaddr.value.nth(0)) == 0); + EXPECT(std::get<1>(*subaddr.value.nth(0)) == 12); + } + + SECTION("merge from one item end") + { + subaddr.value = {{10, 12}}; + subaddr.merge(12); + EXPECT(subaddr.is_valid()); + EXPECT(subaddr.value.size() == 1); + EXPECT(std::get<0>(*subaddr.value.nth(0)) == 0); + EXPECT(std::get<1>(*subaddr.value.nth(0)) == 12); + } + + SECTION("merge from two item split directly") + { + subaddr.value = {{10, 12}, {14, 15}}; + subaddr.merge(13); + EXPECT(subaddr.is_valid()); + EXPECT(subaddr.value.size() == 2); + EXPECT(std::get<0>(*subaddr.value.nth(0)) == 0); + EXPECT(std::get<1>(*subaddr.value.nth(0)) == 13); + EXPECT(std::get<0>(*subaddr.value.nth(1)) == 14); + EXPECT(std::get<1>(*subaddr.value.nth(1)) == 15); + } + + SECTION("merge from two item split gap") + { + subaddr.value = {{10, 12}, {16, 17}}; + subaddr.merge(14); + EXPECT(subaddr.is_valid()); + EXPECT(subaddr.value.size() == 2); + EXPECT(std::get<0>(*subaddr.value.nth(0)) == 0); + EXPECT(std::get<1>(*subaddr.value.nth(0)) == 14); + EXPECT(std::get<0>(*subaddr.value.nth(1)) == 16); + EXPECT(std::get<1>(*subaddr.value.nth(1)) == 17); + } + + SECTION("merge from two item last") + { + subaddr.value = {{10, 12}, {14, 15}}; + subaddr.merge(14); + EXPECT(subaddr.is_valid()); + EXPECT(subaddr.value.size() == 1); + EXPECT(std::get<0>(*subaddr.value.nth(0)) == 0); + EXPECT(std::get<1>(*subaddr.value.nth(0)) == 15); + } + + SECTION("merge from two item append") + { + subaddr.value = {{10, 12}, {14, 15}}; + subaddr.merge(16); + EXPECT(subaddr.is_valid()); + EXPECT(subaddr.value.size() == 1); + EXPECT(std::get<0>(*subaddr.value.nth(0)) == 0); + EXPECT(std::get<1>(*subaddr.value.nth(0)) == 16); + } + } +} +