Skip to content

Commit

Permalink
[MISC] Improve error message when using kmer_hash_view with invalid s…
Browse files Browse the repository at this point in the history
…hapes
  • Loading branch information
eseiler committed Feb 28, 2024
1 parent 3e63456 commit f59e48f
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 20 deletions.
39 changes: 29 additions & 10 deletions include/seqan3/search/views/kmer_hash.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,33 @@ class kmer_hash_view : public std::ranges::view_interface<kmer_hash_view<urng_t>
template <bool const_range>
class basic_iterator;

//!\brief The maximum shape count for the given alphabet.
static inline int max_shape_count = 64 / std::log2(alphabet_size<std::ranges::range_reference_t<urng_t>>);

//!\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<seqan3::dna4>:
// reference_t: seqan3::bitpacked_sequence<seqan3::dna4>::reference_proxy_type
// value_t: seqan3::dna4
message += detail::type_name_as_string<std::remove_cvref_t<std::ranges::range_value_t<urng_t>>>;
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
* \{
Expand All @@ -68,11 +95,7 @@ class kmer_hash_view : public std::ranges::view_interface<kmer_hash_view<urng_t>
*/
explicit kmer_hash_view(urng_t urange_, shape const & s_) : urange{std::move(urange_)}, shape_{s_}
{
if (shape_.count() > (64 / std::log2(alphabet_size<std::ranges::range_reference_t<urng_t>>)))
{
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.
Expand All @@ -86,11 +109,7 @@ class kmer_hash_view : public std::ranges::view_interface<kmer_hash_view<urng_t>
urange{std::views::all(std::forward<rng_t>(urange_))},
shape_{s_}
{
if (shape_.count() > (64 / std::log2(alphabet_size<std::ranges::range_reference_t<urng_t>>)))
{
throw std::invalid_argument{"The chosen shape/alphabet combination is not valid. "
"The alphabet or shape size must be reduced."};
}
validate_shape();
}
//!\}

Expand Down
56 changes: 56 additions & 0 deletions test/include/seqan3/test/expect_throw_msg.hpp
Original file line number Diff line number Diff line change
@@ -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 <enrico.seiler AT fu-berlin.de>
*/

#pragma once

#include <gtest/gtest.h>

#include <seqan3/core/platform.hpp>

#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
42 changes: 32 additions & 10 deletions test/unit/search/views/kmer_hash_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <seqan3/alphabet/views/complement.hpp>
#include <seqan3/search/views/kmer_hash.hpp>
#include <seqan3/test/expect_range_eq.hpp>
#include <seqan3/test/expect_throw_msg.hpp>
#include <seqan3/utility/views/repeat_n.hpp>

#include "../../range/iterator_test_template.hpp"
Expand Down Expand Up @@ -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<TypeParam>) // 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<TypeParam>)
{
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<seqan3::dna5> 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
Expand Down

0 comments on commit f59e48f

Please sign in to comment.