Create Printer - A general-purpose, stateful printer class

This commit is contained in:
Michael 2019-02-24 20:50:49 +00:00
parent 95a5c386fe
commit e6e19369e9
No known key found for this signature in database
GPG Key ID: 2D51757B47E2434C
6 changed files with 879 additions and 19 deletions

View File

@ -19,6 +19,7 @@ set(LIB_UTIL_SRC
util/mem.cpp
util/queue_manager.cpp
util/queue.cpp
util/printer.cpp
util/status.cpp
util/str.cpp
util/string_view.cpp

182
llarp/util/printer.cpp Normal file
View File

@ -0,0 +1,182 @@
#include <util/printer.hpp>
namespace llarp
{
namespace
{
static void
putSpaces(std::ostream& stream, size_t count)
{
// chunk n write
static const char spaces[] = " ";
static constexpr size_t size = sizeof(spaces) - 1;
while(size < count)
{
stream.write(spaces, size);
count -= size;
}
if(count > 0)
{
stream.write(spaces, count);
}
}
} // namespace
Printer::Printer(std::ostream& stream, int level, int spacesPerLevel)
: m_stream(stream)
, m_level(level < 0 ? -level : level)
, m_levelPlusOne(m_level + 1)
, m_suppressIndent(level < 0)
, m_spaces(spacesPerLevel)
{
if(!m_suppressIndent)
{
const int absSpaces = m_spaces < 0 ? -m_spaces : m_spaces;
putSpaces(m_stream, absSpaces * m_level);
}
m_stream << '[';
if(m_spaces >= 0)
{
m_stream << '\n';
}
}
Printer::~Printer()
{
putSpaces(m_stream, m_spaces < 0 ? 1 : m_spaces * m_level);
m_stream << ']';
}
void
Printer::printIndent() const
{
putSpaces(m_stream, m_spaces < 0 ? 1 : m_spaces * m_levelPlusOne);
}
void
Printer::printHexAddr(string_view name, const void* address) const
{
printIndent();
m_stream << name << " = ";
PrintHelper::print(m_stream, address, -m_levelPlusOne, m_spaces);
}
void
Printer::printHexAddr(const void* address) const
{
printIndent();
PrintHelper::print(m_stream, address, -m_levelPlusOne, m_spaces);
}
void
PrintHelper::printType(std::ostream& stream, char value, int,
int spacesPerLevel,
traits::select::Case< std::is_fundamental >)
{
if(std::isprint(static_cast< unsigned char >(value)))
{
stream << "'" << value << "'";
}
else
{
#define PRINT_CONTROL_CHAR(x) \
case x: \
stream << #x; \
break;
switch(value)
{
PRINT_CONTROL_CHAR('\n');
PRINT_CONTROL_CHAR('\t');
PRINT_CONTROL_CHAR('\0');
default:
{
// Print as hex
FormatFlagsGuard guard(stream);
stream << std::hex << std::showbase
<< static_cast< std::uintptr_t >(
static_cast< unsigned char >(value));
}
}
}
if(spacesPerLevel >= 0)
{
stream << '\n';
}
}
void
PrintHelper::printType(std::ostream& stream, bool value, int,
int spacesPerLevel,
traits::select::Case< std::is_fundamental >)
{
{
FormatFlagsGuard guard(stream);
stream << std::boolalpha << value;
}
if(spacesPerLevel >= 0)
{
stream << '\n';
}
}
void
PrintHelper::printType(std::ostream& stream, const char* value, int,
int spacesPerLevel,
traits::select::Case< std::is_pointer >)
{
if(value == nullptr)
{
stream << "null";
}
else
{
stream << '"' << value << '"';
}
if(spacesPerLevel >= 0)
{
stream << '\n';
}
}
void
PrintHelper::printType(std::ostream& stream, const void* value, int,
int spacesPerLevel,
traits::select::Case< std::is_pointer >)
{
if(value == nullptr)
{
stream << "null";
}
else
{
FormatFlagsGuard guard(stream);
stream << std::hex << std::showbase
<< reinterpret_cast< std::uintptr_t >(value);
}
if(spacesPerLevel >= 0)
{
stream << '\n';
}
}
void
PrintHelper::printType(std::ostream& stream, const string_view& value, int,
int spacesPerLevel,
traits::select::Case< traits::is_container >)
{
stream << '"' << value << '"';
if(spacesPerLevel >= 0)
{
stream << '\n';
}
}
} // namespace llarp

511
llarp/util/printer.hpp Normal file
View File

@ -0,0 +1,511 @@
#ifndef LLARP_PRINTER_HPP
#define LLARP_PRINTER_HPP
#include <util/string_view.hpp>
#include <util/traits.hpp>
#include <functional>
#include <iostream>
namespace llarp
{
/// simple guard class to restore stream flags.
struct FormatFlagsGuard
{
std::ios_base& m_base;
std::ios_base::fmtflags m_flags;
FormatFlagsGuard(std::ios_base& base) : m_base(base), m_flags(base.flags())
{
}
~FormatFlagsGuard()
{
m_base.flags(m_flags);
}
};
/// A general-purpose, stateful printer class.
class Printer
{
private:
std::ostream& m_stream;
const int m_level;
const int m_levelPlusOne;
const bool m_suppressIndent;
const int m_spaces;
public:
template < typename Type >
using PrintFunction =
std::function< std::ostream&(std::ostream&, const Type&, int, int) >;
/// Create a printer.
/// - level: the indentation level to use. If negative, suppress indentation
/// on the first line.
/// - spaces: the number of spaces to indent. If negative, put all output on
/// a single line
Printer(std::ostream& stream, int level, int spaces);
~Printer();
/// Print the given `data` to the stream, using the following strategies:
/// - If `Type` is fundamental, print to stream
/// - If `Type` is a C-style array (and not a char array), print each
/// element to the stream
/// - If `Type` is a `void *`, `const void *` or function pointer, and not
/// null, print in hex format or print "null".
/// - If `Type` is a `char *`, a `const char *`, a C-style char array, a
/// `std::string` or `llarp::string_view` print the string wrapped in `"`.
/// - If `Type` is a pointer type, print the pointer, followed by the value
/// if not-null.
/// - If `Type` is a pair/tuple type, print the elements of the tuple.
/// - If `Type` has STL-style iterators, print all elements in the
/// container.
/// - If `Type` is any other type, call the `print` method on that type.
template < typename Type >
void
printAttribute(string_view name, const Type& value) const;
template < typename Type >
void
printAttributeAsHex(string_view name, const Type& value) const;
template < typename InputIt >
void
printAttribute(string_view name, const InputIt& begin,
const InputIt& end) const;
template < typename Type >
void
printValue(const Type& value) const;
template < typename InputIt >
void
printValue(const InputIt& begin, const InputIt& end) const;
template < typename Type >
void
printForeignAttribute(string_view name, const Type& value,
const PrintFunction< Type >& printFunction) const;
template < typename Type >
void
printForeignValue(const Type& value,
const PrintFunction< Type >& printFunction) const;
void
printHexAddr(string_view name, const void* address) const;
void
printHexAddr(const void* address) const;
template < class Type >
void
printOrNull(string_view name, const Type& address) const;
template < class Type >
void
printOrNull(const Type& address) const;
private:
void
printIndent() const;
};
/// helper struct
struct PrintHelper
{
template < typename Type >
static void
print(std::ostream& stream, const Type& value, int level, int spaces);
template < typename InputIt >
static void
print(std::ostream& stream, const InputIt& begin, const InputIt& end,
int level, int spaces);
// Specialisations
// Fundamental types
static void
printType(std::ostream& stream, char value, int level, int spaces,
traits::select::Case< std::is_fundamental >);
static void
printType(std::ostream& stream, bool value, int level, int spaces,
traits::select::Case< std::is_fundamental >);
template < typename Type >
static void
printType(std::ostream& stream, Type value, int level, int spaces,
traits::select::Case< std::is_fundamental >);
template < typename Type >
static void
printType(std::ostream& stream, Type value, int level, int spaces,
traits::select::Case< std::is_enum >);
// Function types
template < typename Type >
static void
printType(std::ostream& stream, Type value, int level, int spaces,
traits::select::Case< std::is_function >);
// Pointer types
static void
printType(std::ostream& stream, const char* value, int level, int spaces,
traits::select::Case< std::is_pointer >);
static void
printType(std::ostream& stream, const void* value, int level, int spaces,
traits::select::Case< std::is_pointer >);
template < typename Type >
static void
printType(std::ostream& stream, const Type* value, int level, int spaces,
traits::select::Case< std::is_pointer >);
template < typename Type >
static void
printType(std::ostream& stream, const Type* value, int level, int spaces,
traits::select::Case< std::is_array >);
// Container types
static void
printType(std::ostream& stream, const std::string& value, int level,
int spaces, traits::select::Case< traits::is_container >);
static void
printType(std::ostream& stream, const string_view& value, int level,
int spaces, traits::select::Case< traits::is_container >);
template < typename Type >
static void
printType(std::ostream& stream, const Type& value, int level, int spaces,
traits::select::Case< traits::is_container >);
// Utility types
template < typename Type1, typename Type2 >
static void
printType(std::ostream& stream, const std::pair< Type1, Type2 >& value,
int level, int spaces, traits::select::Case<>);
template < typename... Types >
static void
printType(std::ostream& stream, const std::tuple< Types... >& value,
int level, int spaces, traits::select::Case<>);
// Default type
template < typename Type >
static void
printType(std::ostream& stream, const Type& value, int level, int spaces,
traits::select::Case<>);
};
template < typename Type >
inline void
Printer::printAttribute(string_view name, const Type& value) const
{
assert(!name.empty());
printIndent();
m_stream << name << " = ";
PrintHelper::print(m_stream, value, -m_levelPlusOne, m_spaces);
}
template < typename Type >
inline void
Printer::printAttributeAsHex(string_view name, const Type& value) const
{
static_assert(std::is_integral< Type >::value, "type should be integral");
assert(!name.empty());
printIndent();
m_stream << name << " = ";
{
FormatFlagsGuard guard(m_stream);
m_stream << std::hex << value;
}
if(m_spaces >= 0)
{
m_stream << '\n';
}
}
template < typename InputIt >
inline void
Printer::printAttribute(string_view name, const InputIt& begin,
const InputIt& end) const
{
assert(!name.empty());
printIndent();
m_stream << name << " = ";
PrintHelper::print(m_stream, begin, end, -m_levelPlusOne, m_spaces);
}
template < typename Type >
inline void
Printer::printValue(const Type& value) const
{
printIndent();
PrintHelper::print(m_stream, value, -m_levelPlusOne, m_spaces);
}
template < typename InputIt >
inline void
Printer::printValue(const InputIt& begin, const InputIt& end) const
{
printIndent();
PrintHelper::print(m_stream, begin, end, -m_levelPlusOne, m_spaces);
}
template < typename Type >
inline void
Printer::printForeignAttribute(
string_view name, const Type& value,
const PrintFunction< Type >& printFunction) const
{
assert(!name.empty());
printIndent();
m_stream << name << " = ";
printFunction(m_stream, value, -m_levelPlusOne, m_spaces);
}
template < typename Type >
inline void
Printer::printForeignValue(const Type& value,
const PrintFunction< Type >& printFunction) const
{
printIndent();
printFunction(m_stream, value, -m_levelPlusOne, m_spaces);
}
template < typename Type >
inline void
Printer::printOrNull(string_view name, const Type& address) const
{
assert(!name.empty());
printIndent();
m_stream << name << " = ";
if(address == nullptr)
{
m_stream << "null";
if(m_spaces >= 0)
{
m_stream << '\n';
}
}
else
{
PrintHelper::print(m_stream, *address, -m_levelPlusOne, m_spaces);
}
}
template < typename Type >
inline void
Printer::printOrNull(const Type& address) const
{
printIndent();
if(address == nullptr)
{
m_stream << "null";
if(m_spaces >= 0)
{
m_stream << '\n';
}
}
else
{
PrintHelper::print(m_stream, *address, -m_levelPlusOne, m_spaces);
}
}
template <>
inline void
Printer::printOrNull< const void* >(string_view name,
const void* const& address) const
{
assert(!name.empty());
printIndent();
m_stream << name << " = ";
const void* temp = address;
PrintHelper::print(m_stream, temp, -m_levelPlusOne, m_spaces);
}
template <>
inline void
Printer::printOrNull< void* >(string_view name, void* const& address) const
{
const void* const& temp = address;
printOrNull(name, temp);
}
template <>
inline void
Printer::printOrNull< const void* >(const void* const& address) const
{
printIndent();
const void* temp = address;
PrintHelper::print(m_stream, temp, -m_levelPlusOne, m_spaces);
}
template <>
inline void
Printer::printOrNull< void* >(void* const& address) const
{
const void* const& temp = address;
printOrNull(temp);
}
// Print Helper methods
template < typename InputIt >
inline void
PrintHelper::print(std::ostream& stream, const InputIt& begin,
const InputIt& end, int level, int spaces)
{
Printer printer(stream, level, spaces);
std::for_each(begin, end, [&](const auto& x) { printer.printValue(x); });
}
template < typename Type >
inline void
PrintHelper::printType(std::ostream& stream, Type value, int, int spaces,
traits::select::Case< std::is_fundamental >)
{
stream << value;
if(spaces >= 0)
{
stream << '\n';
}
}
template < typename Type >
inline void
PrintHelper::printType(std::ostream& stream, Type value, int, int spaces,
traits::select::Case< std::is_enum >)
{
printType(stream, value, 0, spaces,
traits::select::Case< std::is_fundamental >());
}
template < typename Type >
inline void
PrintHelper::printType(std::ostream& stream, Type value, int level,
int spaces, traits::select::Case< std::is_function >)
{
PrintHelper::print(stream, reinterpret_cast< const void* >(value), level,
spaces);
}
template < typename Type >
inline void
PrintHelper::printType(std::ostream& stream, const Type* value, int level,
int spaces, traits::select::Case< std::is_pointer >)
{
printType(stream, static_cast< const void* >(value), level, -1,
traits::select::Case< std::is_pointer >());
if(value == nullptr)
{
if(spaces >= 0)
{
stream << '\n';
}
}
else
{
stream << ' ';
PrintHelper::print(stream, *value, level, spaces);
}
}
template < typename Type >
inline void
PrintHelper::printType(std::ostream& stream, const Type* value, int level,
int spaces, traits::select::Case< std::is_array >)
{
printType(stream, value, level, spaces,
traits::select::Case< std::is_pointer >());
}
inline void
PrintHelper::printType(std::ostream& stream, const std::string& value,
int level, int spaces,
traits::select::Case< traits::is_container >)
{
printType(stream, value.c_str(), level, spaces,
traits::select::Case< std::is_pointer >());
}
template < typename Type >
inline void
PrintHelper::printType(std::ostream& stream, const Type& value, int level,
int spaces,
traits::select::Case< traits::is_container >)
{
print(stream, value.begin(), value.end(), level, spaces);
}
template < typename Type1, typename Type2 >
inline void
PrintHelper::printType(std::ostream& stream,
const std::pair< Type1, Type2 >& value, int level,
int spaces, traits::select::Case<>)
{
Printer print(stream, level, spaces);
print.printValue(value.first);
print.printValue(value.second);
}
template < typename... Types >
inline void
PrintHelper::printType(std::ostream& stream,
const std::tuple< Types... >& value, int level,
int spaces, traits::select::Case<>)
{
Printer print(stream, level, spaces);
traits::for_each_in_tuple(value,
[&](const auto& x) { print.printValue(x); });
}
template < typename Type >
inline void
PrintHelper::printType(std::ostream& stream, const Type& value, int level,
int spaces, traits::select::Case<>)
{
value.print(stream, level, spaces);
}
template < typename Type >
inline void
PrintHelper::print(std::ostream& stream, const Type& value, int level,
int spaces)
{
using Selection =
traits::select::Select< Type, std::is_fundamental, std::is_enum,
std::is_function, std::is_pointer,
std::is_array, traits::is_container >;
PrintHelper::printType(stream, value, level, spaces, Selection());
}
} // namespace llarp
#endif

View File

@ -2,6 +2,7 @@
#define LLARP_TRAITS_HPP
#include <type_traits>
#include <utility>
namespace llarp
{
@ -93,15 +94,15 @@ namespace llarp
/// implementation helper
template < typename T,
template < typename > class Trait1,
template < typename > class Trait2,
template < typename > class Trait3,
template < typename > class Trait4,
template < typename > class Trait5,
template < typename > class Trait6,
template < typename > class Trait7,
template < typename > class Trait8,
template < typename > class Trait9 >
template < typename... > class Trait1,
template < typename... > class Trait2,
template < typename... > class Trait3,
template < typename... > class Trait4,
template < typename... > class Trait5,
template < typename... > class Trait6,
template < typename... > class Trait7,
template < typename... > class Trait8,
template < typename... > class Trait9 >
struct SelectHelper {
enum {
Selector = (
@ -133,15 +134,15 @@ namespace llarp
};
template< typename Type,
template < typename > class Trait1,
template < typename > class Trait2 = False,
template < typename > class Trait3 = False,
template < typename > class Trait4 = False,
template < typename > class Trait5 = False,
template < typename > class Trait6 = False,
template < typename > class Trait7 = False,
template < typename > class Trait8 = False,
template < typename > class Trait9 = False >
template < typename... > class Trait1,
template < typename... > class Trait2 = False,
template < typename... > class Trait3 = False,
template < typename... > class Trait4 = False,
template < typename... > class Trait5 = False,
template < typename... > class Trait6 = False,
template < typename... > class Trait7 = False,
template < typename... > class Trait8 = False,
template < typename... > class Trait9 = False >
struct Select : public SelectHelper< Type,
Trait1,
Trait2,
@ -171,7 +172,26 @@ namespace llarp
// clang-format on
} // namespace select
} // namespace traits
namespace detail
{
template < typename T, typename F, size_t... Is >
void
for_each(T&& t, F f, std::index_sequence< Is... >)
{
auto l = {(f(std::get< Is >(t)), 0)...};
(void)l;
}
} // namespace detail
template < typename... Ts, typename F >
void
for_each_in_tuple(std::tuple< Ts... > const& t, F f)
{
detail::for_each(t, f, std::make_index_sequence< sizeof...(Ts) >());
}
} // namespace traits
} // namespace llarp
#endif

View File

@ -34,6 +34,7 @@ list(APPEND TEST_SRC
util/test_llarp_util_bits.cpp
util/test_llarp_util_encode.cpp
util/test_llarp_util_ini.cpp
util/test_llarp_util_printer.cpp
util/test_llarp_util_queue_manager.cpp
util/test_llarp_util_queue.cpp
util/test_llarp_util_thread_pool.cpp

View File

@ -0,0 +1,145 @@
#include <util/printer.hpp>
#include <absl/types/variant.h>
#include <unordered_map>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
using namespace llarp;
using namespace ::testing;
struct PrintableType
{
std::ostream &
print(std::ostream &stream, int level, int spaces) const
{
stream << "PrintableType " << level << " " << spaces;
return stream;
}
};
using SingleVariant =
absl::variant< char, bool, short, int, unsigned int, const void *,
const char *, std::string, const int *,
std::pair< int, std::string >,
std::tuple< int, std::string, int >,
std::map< std::string, char >, PrintableType >;
using SingleType = std::pair< SingleVariant, Matcher< std::string > >;
class SingleValueTest : public ::testing::TestWithParam< SingleType >
{
};
TEST_P(SingleValueTest, value)
{
SingleType d = GetParam();
std::ostringstream stream;
{
Printer printer(stream, -1, -1);
absl::visit([&](const auto &x) { printer.printValue(x); }, d.first);
}
ASSERT_THAT(stream.str(), d.second);
}
static const char PTR_TYPE[] = "abacus";
static const int INT_VAL = 100;
// clang-format off
static const SingleType singleType[] = {
{char('a'), StrEq("[ 'a' ]")},
{bool(true), StrEq("[ true ]")},
{bool(false), StrEq("[ false ]")},
{short(123), StrEq("[ 123 ]")},
{int(INT_MAX - 1), StrEq("[ 2147483646 ]")},
{static_cast< unsigned int >(std::numeric_limits< int >::max()) + 1, StrEq("[ 2147483648 ]")},
{static_cast< const void * >(PTR_TYPE), AllOf(StartsWith("[ 0x"), EndsWith(" ]"))},
{static_cast< const char * >(PTR_TYPE), StrEq("[ \"abacus\" ]")},
{std::string("abacus"), StrEq("[ \"abacus\" ]")},
{static_cast< const int * >(&INT_VAL), AllOf(StartsWith("[ 0x"), EndsWith(" ]"))},
{std::pair< int, std::string >(100, "abacus"), StrEq("[ [ 100 \"abacus\" ] ]")},
{std::tuple< int, std::string, int >(100, "abacus", 123), StrEq("[ [ 100 \"abacus\" 123 ] ]")},
{std::map< std::string, char >{{"one", 'a'}, {"two", 'b'}, {"three", 'c'}}, StrEq("[ [ [ \"one\" \'a\' ] [ \"three\" \'c\' ] [ \"two\" 'b' ] ] ]")},
{PrintableType(), StrEq("[ PrintableType -2 -1 ]")},
};
// clang-format on
INSTANTIATE_TEST_CASE_P(Printer, SingleValueTest,
::testing::ValuesIn(singleType), );
using SingleAttributeType =
std::tuple< std::string, SingleVariant, Matcher< std::string > >;
class SingleAttributeTest
: public ::testing::TestWithParam< SingleAttributeType >
{
};
TEST_P(SingleAttributeTest, value)
{
SingleAttributeType d = GetParam();
std::ostringstream stream;
{
Printer printer(stream, -1, -1);
absl::visit(
[&](const auto &x) { printer.printAttribute(std::get< 0 >(d), x); },
std::get< 1 >(d));
}
ASSERT_THAT(stream.str(), std::get< 2 >(d));
}
// clang-format off
static const SingleAttributeType singleAttributeType[] = {
{"our_value", char('a'), StrEq("[ our_value = 'a' ]")},
{"our_value", bool(true), StrEq("[ our_value = true ]")},
{"our_value", bool(false), StrEq("[ our_value = false ]")},
{"our_value", short(123), StrEq("[ our_value = 123 ]")},
{"our_value", int(INT_MAX - 1), StrEq("[ our_value = 2147483646 ]")},
{"our_value", static_cast< unsigned int >(std::numeric_limits< int >::max()) + 1, StrEq("[ our_value = 2147483648 ]")},
{"our_value", static_cast< const void * >(PTR_TYPE), AllOf(StartsWith("[ our_value = 0x"), EndsWith(" ]"))},
{"our_value", static_cast< const char * >(PTR_TYPE), StrEq("[ our_value = \"abacus\" ]")},
{"our_value", std::string("abacus"), StrEq("[ our_value = \"abacus\" ]")},
{"our_value", static_cast< const int * >(&INT_VAL), AllOf(StartsWith("[ our_value = 0x"), EndsWith(" ]"))},
{"our_value", std::pair< int, std::string >(100, "abacus"), StrEq("[ our_value = [ 100 \"abacus\" ] ]")},
{"our_value", std::tuple< int, std::string, int >(100, "abacus", 123), StrEq("[ our_value = [ 100 \"abacus\" 123 ] ]")},
{"our_value", std::map< std::string, char >{{"one", 'a'}, {"two", 'b'}, {"three", 'c'}}, StrEq("[ our_value = [ [ \"one\" \'a\' ] [ \"three\" \'c\' ] [ \"two\" 'b' ] ] ]")},
{"our_value", PrintableType(), StrEq("[ our_value = PrintableType -2 -1 ]")},
};
// clang-format on
INSTANTIATE_TEST_CASE_P(Printer, SingleAttributeTest,
::testing::ValuesIn(singleAttributeType), );
using ManyAttributes =
std::pair< std::vector< std::pair< std::string, SingleVariant > >,
Matcher< std::string > >;
class ManyAttributesTest : public ::testing::TestWithParam< ManyAttributes >
{
};
TEST_P(ManyAttributesTest, value)
{
ManyAttributes d = GetParam();
std::ostringstream stream;
{
Printer printer(stream, -1, -1);
std::for_each(d.first.begin(), d.first.end(), [&](const auto &y) {
std::string n = y.first;
const auto &v = y.second;
absl::visit([&](const auto &x) { printer.printAttribute(n, x); }, v);
});
}
ASSERT_THAT(stream.str(), d.second);
}
// clang-format off
static const ManyAttributes manyAttributes[] = {
{{{"val", 1}, {"v2", 2}, {"v3", 3}, {"str", std::string("xxx")}}, StrEq("[ val = 1 v2 = 2 v3 = 3 str = \"xxx\" ]")},
{{{"str", std::string("xxx")}}, StrEq("[ str = \"xxx\" ]")}
};
// clang-format on
INSTANTIATE_TEST_CASE_P(Printer, ManyAttributesTest,
::testing::ValuesIn(manyAttributes), );