From f59e48fc1c90fd2cb052787c872256123b7e8ce1 Mon Sep 17 00:00:00 2001 From: Enrico Seiler Date: Wed, 28 Feb 2024 12:07:44 +0100 Subject: [PATCH] [MISC] Improve error message when using kmer_hash_view with invalid shapes --- include/seqan3/search/views/kmer_hash.hpp | 39 +++++++++---- test/include/seqan3/test/expect_throw_msg.hpp | 56 +++++++++++++++++++ test/unit/search/views/kmer_hash_test.cpp | 42 ++++++++++---- 3 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 test/include/seqan3/test/expect_throw_msg.hpp diff --git a/include/seqan3/search/views/kmer_hash.hpp b/include/seqan3/search/views/kmer_hash.hpp index 48276e80fa..b77876471a 100644 --- a/include/seqan3/search/views/kmer_hash.hpp +++ b/include/seqan3/search/views/kmer_hash.hpp @@ -49,6 +49,33 @@ class kmer_hash_view : public std::ranges::view_interface template class basic_iterator; + //!\brief The maximum shape count for the given alphabet. + static inline int max_shape_count = 64 / std::log2(alphabet_size>); + + //!\brief Throws the exception for validate_shape(). + void shape_too_long_error() const + { + std::string message{"The shape is too long for the given alphabet.\n"}; + message += "Alphabet: "; + // Note: Since we want the alphabet type name, std::ranges::range_value_t is the better choice. + // For seqan3::bitpacked_sequence: + // reference_t: seqan3::bitpacked_sequence::reference_proxy_type + // value_t: seqan3::dna4 + message += detail::type_name_as_string>>; + message += "\nMaximum shape count: "; + message += std::to_string(max_shape_count); + message += "\nGiven shape count: "; + message += std::to_string(shape_.count()); + throw std::invalid_argument{message}; + } + + //!\brief Checks that the shape is not too long for the given alphabet. + inline void validate_shape() const + { + if (shape_.count() > max_shape_count) + shape_too_long_error(); + } + public: /*!\name Constructors, destructor and assignment * \{ @@ -68,11 +95,7 @@ class kmer_hash_view : public std::ranges::view_interface */ explicit kmer_hash_view(urng_t urange_, shape const & s_) : urange{std::move(urange_)}, shape_{s_} { - if (shape_.count() > (64 / std::log2(alphabet_size>))) - { - throw std::invalid_argument{"The chosen shape/alphabet combination is not valid. " - "The alphabet or shape size must be reduced."}; - } + validate_shape(); } /*!\brief Construct from a non-view that can be view-wrapped and a given shape. @@ -86,11 +109,7 @@ class kmer_hash_view : public std::ranges::view_interface urange{std::views::all(std::forward(urange_))}, shape_{s_} { - if (shape_.count() > (64 / std::log2(alphabet_size>))) - { - throw std::invalid_argument{"The chosen shape/alphabet combination is not valid. " - "The alphabet or shape size must be reduced."}; - } + validate_shape(); } //!\} diff --git a/test/include/seqan3/test/expect_throw_msg.hpp b/test/include/seqan3/test/expect_throw_msg.hpp new file mode 100644 index 0000000000..8f0f131d28 --- /dev/null +++ b/test/include/seqan3/test/expect_throw_msg.hpp @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2006-2024, Knut Reinert & Freie Universität Berlin +// SPDX-FileCopyrightText: 2016-2024, Knut Reinert & MPI für molekulare Genetik +// SPDX-License-Identifier: BSD-3-Clause + +/*!\file + * \brief Provides EXPECT_THROW_MSG. + * \author Enrico Seiler + */ + +#pragma once + +#include + +#include + +#ifdef EXPECT_THROW_MSG +# warning "EXPECT_THROW_MSG is already defined." +#else +# define EXPECT_THROW_MSG(statement, expected_exception, expected_message) \ + try \ + { \ + statement; \ + std::string const message = "Expected: " #statement " throws an exception of type " #expected_exception \ + ".\n Actual: it throws nothing."; \ + GTEST_NONFATAL_FAILURE_(message.data()); \ + } \ + catch (expected_exception const & exception) \ + { \ + if (auto result = ::testing::internal::EqHelper::Compare("Expected", \ + "Actual", \ + std::string_view{expected_message}, \ + std::string_view{exception.what()}); \ + !result) \ + { \ + std::string message = #statement " throws the correct exception, but the description is incorrect.\n"; \ + message += result.failure_message(); \ + GTEST_NONFATAL_FAILURE_(message.data()); \ + } \ + } \ + catch (std::exception const & exception) \ + { \ + std::string message = "Expected: " #statement " throws an exception of type " #expected_exception ".\n "; \ + message += "Actual: it throws "; \ + message += ::testing::internal::GetTypeName(typeid(exception)); \ + message += " with description \""; \ + message += exception.what(); \ + message += "\"."; \ + GTEST_NONFATAL_FAILURE_(message.data()); \ + } \ + catch (...) \ + { \ + std::string message = "Expected: " #statement " throws an exception of type " #expected_exception ".\n "; \ + message += "Actual: it throws an unknown exception."; \ + GTEST_NONFATAL_FAILURE_(message.data()); \ + } +#endif diff --git a/test/unit/search/views/kmer_hash_test.cpp b/test/unit/search/views/kmer_hash_test.cpp index 0c6fa58411..a7decadefe 100644 --- a/test/unit/search/views/kmer_hash_test.cpp +++ b/test/unit/search/views/kmer_hash_test.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "../../range/iterator_test_template.hpp" @@ -149,22 +150,43 @@ TYPED_TEST(kmer_hash_gapped_test, concepts) TYPED_TEST(kmer_hash_ungapped_test, invalid_sizes) { - TypeParam text1{'A'_dna4, 'A'_dna4, 'A'_dna4, 'A'_dna4, 'A'_dna4}; - EXPECT_NO_THROW(text1 | seqan3::views::kmer_hash(seqan3::ungapped{32})); - EXPECT_THROW(text1 | seqan3::views::kmer_hash(seqan3::ungapped{33}), std::invalid_argument); - if constexpr (std::ranges::bidirectional_range) // excludes forward_list + auto expected_error_message = + [](std::string_view const alphabet, size_t const max_shape_count, size_t const given_shape_count) + { + std::string message{"The shape is too long for the given alphabet.\n"}; + message += "Alphabet: "; + message += alphabet; + message += "\nMaximum shape count: "; + message += std::to_string(max_shape_count); + message += "\nGiven shape count: "; + message += std::to_string(given_shape_count); + return message; + }; + + TypeParam text{}; + EXPECT_NO_THROW(text | seqan3::views::kmer_hash(seqan3::ungapped{32})); + EXPECT_THROW_MSG(text | seqan3::views::kmer_hash(seqan3::ungapped{33}), + std::invalid_argument, + expected_error_message("seqan3::dna4", 32, 33)); + + if constexpr (std::ranges::bidirectional_range) { - EXPECT_NO_THROW(text1 | std::views::reverse | seqan3::views::kmer_hash(seqan3::ungapped{32})); - EXPECT_THROW(text1 | std::views::reverse | seqan3::views::kmer_hash(seqan3::ungapped{33}), - std::invalid_argument); + EXPECT_NO_THROW(text | std::views::reverse | seqan3::views::kmer_hash(seqan3::ungapped{32})); + EXPECT_THROW_MSG(text | std::views::reverse | seqan3::views::kmer_hash(seqan3::ungapped{33}), + std::invalid_argument, + expected_error_message("seqan3::dna4", 32, 33)); } - EXPECT_NO_THROW(text1 | seqan3::views::kmer_hash(0xFFFFFFFE001_shape)); // size=44, count=32 - EXPECT_THROW(text1 | seqan3::views::kmer_hash(0xFFFFFFFFE009_shape), std::invalid_argument); // size=44, count=33 + EXPECT_NO_THROW(text | seqan3::views::kmer_hash(0xFFFFFFFE001_shape)); // size=48, count=32 + EXPECT_THROW_MSG(text | seqan3::views::kmer_hash(0xFFFFFFFE0009_shape), // size=48, count=33 + std::invalid_argument, + expected_error_message("seqan3::dna4", 32, 33)); std::vector dna5_text{}; EXPECT_NO_THROW(dna5_text | seqan3::views::kmer_hash(seqan3::ungapped{27})); - EXPECT_THROW(dna5_text | seqan3::views::kmer_hash(seqan3::ungapped{28}), std::invalid_argument); + EXPECT_THROW_MSG(dna5_text | seqan3::views::kmer_hash(seqan3::ungapped{28}), + std::invalid_argument, + expected_error_message("seqan3::dna5", 27, 28)); } // https://github.com/seqan/seqan3/issues/1614