Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[MISC] Improve error message when using kmer_hash_view with invalid shapes #3244

Merged
merged 1 commit into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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().
[[noreturn]] 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
Loading