include/cuda/experimental/__stf/utility/unittest.cuh
File members: include/cuda/experimental/__stf/utility/unittest.cuh
//===----------------------------------------------------------------------===//
//
// Part of CUDASTF in CUDA C++ Core Libraries,
// under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES.
//
//===----------------------------------------------------------------------===//
#pragma once
#include <cuda/__cccl_config>
#if defined(_CCCL_IMPLICIT_SYSTEM_HEADER_GCC)
# pragma GCC system_header
#elif defined(_CCCL_IMPLICIT_SYSTEM_HEADER_CLANG)
# pragma clang system_header
#elif defined(_CCCL_IMPLICIT_SYSTEM_HEADER_MSVC)
# pragma system_header
#endif // no system header
#include <cuda/std/source_location>
#include <cuda/experimental/__stf/utility/traits.cuh>
#include <filesystem>
#ifndef DOXYGEN_SHOULD_SKIP_THIS // Do not document
// One level of macro indirection is required in order to resolve __COUNTER__,
// and get varname1 instead of varname__COUNTER__.
# define _55f56f4e3b45c8cf3fa50b28fed72e2a(a, b) _a56ec7069122ad2e0888a508ecdc4639(a, b)
# define _a56ec7069122ad2e0888a508ecdc4639(a, b) a##b
# define CUDASTF_UNIQUE_NAME(base) _55f56f4e3b45c8cf3fa50b28fed72e2a(base, __COUNTER__)
# define EXPECT(...) \
::cuda::experimental::stf::expecter::validate( \
_CUDA_VSTD::source_location::current(), ::cuda::experimental::stf::expecter()->*__VA_ARGS__)
namespace cuda::experimental::stf
{
/*
* @brief Entry point for all comparisons (equality, inequality, and ordering) tested.
*
*/
struct expecter
{
/*
* @brief Exception embedding location information.
*
*/
struct failure : ::std::runtime_error
{
template <typename... Msgs>
failure(_CUDA_VSTD::source_location loc, const Msgs&... msgs)
: ::std::runtime_error(text(loc, msgs...))
{}
template <typename... Msgs>
static ::std::string text(_CUDA_VSTD::source_location loc, const Msgs&... msgs)
{
::std::stringstream s;
s << loc.file_name() << '(' << loc.line() << "): ";
(stream(s, msgs), ...);
auto result = s.str();
if (!result.empty() && result.back() != '\n')
{
result += '\n';
}
return result;
}
private:
template <typename T>
static void stream(::std::stringstream& s, const T& x)
{
if constexpr (reserved::has_ostream_operator<T>::value)
{
s << x;
}
else
{
s << type_name<T> << "{?}";
}
}
};
/*
* @brief Wrapper for a comparison operation using one of `==`, `!=`, `<`, `>`, `<=`, `>=`. Includes the result and
* the operands.
*
* @tparam T Type of the left-hand side operator (qualifiers and reference included)
* @tparam U Type of the right-hand side operator (qualifiers and reference included)
*/
template <typename L, typename R>
struct comparison_expression
{
bool value;
L lhs;
R rhs;
const char* op;
comparison_expression(bool value, L lhs, R rhs, const char* op)
: value(value)
, lhs(::std::forward<L>(lhs))
, rhs(::std::forward<R>(rhs))
, op(op)
{}
};
/*
* @brief Wraps a single term (the left-hand side one) in a comparison expression.
*
* @tparam T type of the term, may include a reference or a const reference etc.
*/
template <typename T>
struct term
{
T value;
template <typename U>
term(U&& value)
: value(::std::forward<U>(value))
{}
/*
* All comparison operators are defined assuming `const&` inputs. They are evaluated eagerly and return a
* `compariso_expression` object with information necessary for constructing an error message, if any.
*/
# define _9d10c7e37932af3c4f39a5ce7ff00b5a(op) \
template <typename U> \
auto operator op(const U& rhs)&& \
{ \
using T_noref = std::remove_reference_t<T>; \
using U_noref = std::remove_reference_t<U>; \
if constexpr (std::is_integral_v<T_noref> && std::is_integral_v<U_noref> \
&& std::is_signed_v<T_noref> != std::is_signed_v<U_noref>) \
{ \
using CommonType = std::common_type_t<T_noref, U_noref>; \
const bool r = static_cast<CommonType>(value) op static_cast<CommonType>(rhs); \
return comparison_expression<T, const U&>(r, std::forward<T>(value), rhs, #op); \
} \
else \
{ \
/* Direct comparison for other cases */ \
const bool r = value op rhs; \
return comparison_expression<T, const U&>(r, std::forward<T>(value), rhs, #op); \
} \
}
_9d10c7e37932af3c4f39a5ce7ff00b5a(==);
_9d10c7e37932af3c4f39a5ce7ff00b5a(!=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(<);
_9d10c7e37932af3c4f39a5ce7ff00b5a(>);
_9d10c7e37932af3c4f39a5ce7ff00b5a(<=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(>=);
# undef _9d10c7e37932af3c4f39a5ce7ff00b5a
/*
* Operators `^`, `|`, `*`, `/`, `%`, `+`, `-`, `>>`, and `<<` are defined only for carrying the actual
* computation alongside with the `term` wrapper. For example, in the expression `EXPECT(a+b==c)`, which expands
* to `expecter()->*a+b==c`, the operator+ defined below will make sure `a+b` is correctly computed before
* evaluating
* `==`.
*/
# define _9d10c7e37932af3c4f39a5ce7ff00b5a(op) \
template <typename U> \
auto operator op(U&& rhs)&& \
{ \
using Result = decltype(value op ::std::forward<U>(rhs)); \
return term<Result>(::std::forward<T>(value) op ::std::forward<U>(rhs)); \
}
_9d10c7e37932af3c4f39a5ce7ff00b5a(*);
_9d10c7e37932af3c4f39a5ce7ff00b5a(/);
_9d10c7e37932af3c4f39a5ce7ff00b5a(%);
_9d10c7e37932af3c4f39a5ce7ff00b5a(+);
_9d10c7e37932af3c4f39a5ce7ff00b5a(-);
_9d10c7e37932af3c4f39a5ce7ff00b5a(>>);
_9d10c7e37932af3c4f39a5ce7ff00b5a(<<);
_9d10c7e37932af3c4f39a5ce7ff00b5a(&);
_9d10c7e37932af3c4f39a5ce7ff00b5a(^);
_9d10c7e37932af3c4f39a5ce7ff00b5a(|);
_9d10c7e37932af3c4f39a5ce7ff00b5a(=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(+=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(-=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(*=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(/=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(%=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(<<=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(>>=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(&=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(^=);
_9d10c7e37932af3c4f39a5ce7ff00b5a(|=);
# undef _9d10c7e37932af3c4f39a5ce7ff00b5a
/*
* Operators `&&`, `||`, `,` do not obey order of evaluation and are disallowed at top level. For example you
* cannot write `EXPECT(a && b)` - instead, you could write `EXPECT((a && b))` but you lose information about
* which side of the conjunction failed.
*/
template <typename U>
auto operator&&(U&&) = delete;
template <typename U>
auto operator||(U&&) = delete;
template <typename U>
auto operator,(U&&) = delete;
};
template <typename T>
term<T> operator->*(T&& lhs)
{
return term<T>(::std::forward<T>(lhs));
}
/*
* @brief Validates a single-term expression, as in `EXPECT(v.empty())`.
*
* @tparam T the term's type including qualifiers
* @param loc source location for error reporting
* @param t term value, an exception will be thrown if non-true
*/
template <typename T, typename... Msgs>
static decltype(auto) validate(_CUDA_VSTD::source_location loc, term<T>&& t, const Msgs&... msgs)
{
if (t.value)
{
return ::std::forward<T>(t.value);
}
if constexpr (sizeof...(msgs) == 0)
{
using U = ::std::remove_reference_t<T>;
throw failure(
loc,
"Tested expression of type " + ::std::string(type_name<T>) + " is "
+ (std::is_same_v<const U, const bool> ? "false"
: ::std::is_arithmetic_v<U> ? "zero"
: "null")
+ ".\n");
}
else
{
throw failure(loc, msgs...);
}
}
/*
* @brief Validates a comparison using `==`, `!=` etc.
*
* @tparam L type of left-hand side operand
* @tparam R type of left-hand side operand
* @param loc source code location
* @param e expression, an exception will be thrown is `e.value` is false
*/
template <typename L, typename R, typename... Msgs>
static decltype(auto) validate(_CUDA_VSTD::source_location loc, comparison_expression<L, R>&& e, const Msgs&... msgs)
{
if (e.value)
{
return ::std::forward<L>(e.lhs);
}
throw failure(loc, e.lhs, ' ', e.op, ' ', e.rhs, " is false.\n", msgs...);
}
};
/*
* @brief Small object that runs a unittest
*
* Typically not used directly; just use `UNITTEST` (see below) to define and run tests.
*
* Also includes static globals for test assertions and for counting pass/fail unittests.
*/
template <typename...>
class unittest;
/*
* @brief Parameterless unittest implementation
*
* Typically not used directly; just use `UNITTEST` (see below) to define and run tests.
*
* Most unittests are not parameterized. In addition, all parameterized unittests ultimately inherit `unittest<>` so
* this class has most of the unittest paraphernalia within.
*/
template <>
class unittest<>
{
public:
/*
* @brief Constructor (to be used with the `UNITTEST` macro) taking a name and the current location.
*
* @param name name of the unittest, e.g. `UNITTEST("name goes here") { ... };`
* @param loc current location of the use of `UNITTEST`, automatically provided by the `UNITTEST` macro.
*/
unittest(const char* name, _CUDA_VSTD::source_location loc)
: name(name)
, loc(mv(loc))
{}
/*
* @brief Factory method that creates a (potentially parameterized) unittest from variadic arguments.
*
* @tparam Params types of unittest parameters, if any (multiple types are allowed)
* @param name name of the unittest
* @param loc code location
* @param params unittest parameters, if any
* @return unittest<Params...> the unittest object with inferred type and properly initialized
*
* This function is not used directly. Instead, the invocation `UNITTEST("name", 1, 2.2)` calls
* `unittest<>::make("name", cuda::std::source_location::current(), 1, 2,2)`.
*/
template <typename... Params>
static unittest<Params...> make(const char* name, _CUDA_VSTD::source_location loc, Params&&... params)
{
return unittest<Params...>(name, loc, ::std::forward<Params>(params)...);
}
public:
/*
* @brief Unittest entry point, to be used with the `UNITTEST` macro.
*
* @tparam Fun type of lambda containing the unittest
* @param fun lambda containing the unittest
* @return unittest& `*this`
*/
template <typename Fun>
unittest& operator->*([[maybe_unused]] Fun&& fun)
{
# ifdef UNITTESTED_FILE
if (!::std::filesystem::equivalent(loc.file_name(), UNITTESTED_FILE))
{
return *this;
}
++total();
try
{
fun(name);
++passed();
// fprintf(stdout, "%s(%zu): PASS(%s)\n", filename.c_str(), line, name);
}
catch (const expecter::failure& e)
{
fprintf(stderr, "%sUNITTEST FAILURE: %s\n", e.what(), name);
}
catch (const ::std::exception& e)
{
fprintf(stderr, "%s(%u): %s\nUNITTEST FAILURE: %s\n", loc.file_name(), loc.line(), e.what(), name);
}
# endif
return *this;
}
public:
static size_t& passed()
{
static size_t result;
return result;
}
static size_t& total()
{
static size_t result;
return result;
}
protected:
const char* const name;
const _CUDA_VSTD::source_location loc;
};
/*
* @brief Implementation of `unittest` for one or more parameters.
*
* @tparam Param first parameter type (may include qualifiers and adornments)
* @tparam Params other parameters, if any (may include qualifiers and adornments) (may be empty)
*
* The hierarchy is set up in a linear fashion: `unittest<int, double>` inherits `unittest<double>` which in turn
* inherits `unittest<>`.
*/
template <typename Param, typename... Params>
class unittest<Param, Params...> : public unittest<Params...>
{
public:
unittest(const char* name, _CUDA_VSTD::source_location loc, Param param, Params... params)
: unittest<Params...>(name, loc, ::std::forward<Params>(params)...)
, param(::std::forward<Param>(param))
{}
template <typename Fun>
unittest& operator->*(Fun&& fun)
{
unittest<>::operator->*([&](const char* name) {
fun(name, ::std::forward<Param>(param));
});
if constexpr (sizeof...(Params) > 0)
{
unittest<Params...>::operator->*(::std::forward<Fun>(fun));
}
return *this;
}
private:
Param param;
};
} // namespace cuda::experimental::stf
// Try to detect when __VA_OPT__ is available
# if defined(__cplusplus) && __cplusplus >= 202002L
# define STF_HAS_UNITTEST_WITH_ARGS 1
# endif
# ifdef UNITTESTED_FILE
int main()
{
using namespace cuda::experimental::stf;
if (unittest<>::total() != unittest<>::passed())
{
fprintf(stderr, "FAIL: %zu of %zu\n", unittest<>::total() - unittest<>::passed(), unittest<>::total());
return 1;
}
// fprintf(stdout, "PASS: %zu of %zu\n", passed(), total());
}
# ifdef STF_HAS_UNITTEST_WITH_ARGS
# define UNITTEST(name, ...) \
[[maybe_unused]] static const auto CUDASTF_UNIQUE_NAME(unittest) = \
::cuda::experimental::stf::unittest<>::make( \
name, _CUDA_VSTD::source_location::current() __VA_OPT__(, __VA_ARGS__)) \
->*[]([[maybe_unused]] const char* unittest_name __VA_OPT__(, [[maybe_unused]] auto&& unittest_param))
# else
# define UNITTEST(name) \
[[maybe_unused]] static const auto CUDASTF_UNIQUE_NAME(unittest) = \
::cuda::experimental::stf::unittest<>::make(name, _CUDA_VSTD::source_location::current())->*[ \
]([[maybe_unused]] const char* unittest_name)
# endif
# else
# ifdef STF_HAS_UNITTEST_WITH_ARGS
# define UNITTEST(name, ...) \
[[maybe_unused]] static const auto CUDASTF_UNIQUE_NAME(unused_unittest) = \
[]([[maybe_unused]] auto&& unittest_name __VA_OPT__(, [[maybe_unused]] auto&& unittest_param))
# else
# define UNITTEST(name) \
[[maybe_unused]] static const auto CUDASTF_UNIQUE_NAME(unused_unittest) = \
[]([[maybe_unused]] auto&& unittest_name)
# endif
# endif
// Just making sure that core functionality works
# ifdef STF_HAS_UNITTEST_WITH_ARGS
UNITTEST("Numeric NOP test.", int(), float(), double())
{
using T = ::std::remove_reference_t<decltype(unittest_param)>;
auto x = EXPECT(T(1) + T(1) == T(2));
static_assert(::std::is_same_v<decltype(x), T>);
};
# endif // STF_HAS_UNITTEST_WITH_ARGS
UNITTEST("EXPECT")
{
EXPECT(1 == 1); // pass
EXPECT(1 + 1 == 2, "All we know about mathematics has been upended!"); // pass
auto p = EXPECT(malloc(100) != nullptr); // pass, put the pointer to allocated memory in p
free(p);
try
{
EXPECT(41 == 102); // fail, will throw exception
}
catch (const ::std::exception& e)
{
// The exception message will contain the actual numbers 41 and 102.
EXPECT(::std::string_view(e.what()).find("41 == 102 is false") != ::std::string_view::npos);
}
};
/* Warning! All occurrences of `babe699bbb083d2b0aac2460e75bca96` below will be replaced with `UNITTEST` in
* postprocessing. This is because we instructed doxygen to NOT generate documentation for the symbol UNITTEST, but we
* still want to have the definition of the macro show up in the documentation. So we define
* `babe699bbb083d2b0aac2460e75bca96`, document it, then replace it with `UNITTEST` in all doxygen-generated files.
*/
# define babe699bbb083d2b0aac2460e75bca96(name, ...)
# undef babe699bbb083d2b0aac2460e75bca96
UNITTEST("Simple unittest")
{
EXPECT(1 != 2);
};
# ifdef STF_HAS_UNITTEST_WITH_ARGS
UNITTEST("Parameterized unittest", 42, 42L, 42.0f, 42.0)
{
// In case the type of the parameter is needed, it can be accessed as follows:
// using T = ::std::remove_reference_t<decltype(unittest_param)>;
EXPECT(unittest_param == 42);
};
# endif // STF_HAS_UNITTEST_WITH_ARGS
// Documentation unittest for traits.h
UNITTEST("type_name")
{
using namespace cuda::experimental::stf;
EXPECT(type_name<int> == "int");
EXPECT(type_name<double> == "double");
};
// Another documentation unittest for traits.h
UNITTEST("tuple2tuple")
{
using namespace cuda::experimental::stf;
auto t = ::std::make_tuple(1, 2, 3);
auto t1 = tuple2tuple(t, [](auto x) {
return x + 1.0;
});
static_assert(::std::is_same_v<decltype(t1), ::std::tuple<double, double, double>>);
EXPECT(t1 == ::std::make_tuple(2.0, 3.0, 4.0));
};
UNITTEST("only_convertible")
{
using namespace cuda::experimental::stf;
auto a = only_convertible<int>(1, "hello");
EXPECT(a == 1);
};
UNITTEST("all_convertible")
{
using namespace cuda::experimental::stf;
auto a = all_convertible<int>(1, 2, "hello", 3);
EXPECT((a == ::std::array<int, 3>{1, 2, 3}));
};
UNITTEST("shuffled arguments")
{
using namespace cuda::experimental::stf;
auto x0 = only_convertible_or(42);
EXPECT(x0 == 42);
auto x1 = only_convertible_or(42, 23);
EXPECT(x1 == 23);
auto x2 = only_convertible_or(42, "23", ::std::tuple{1});
EXPECT(x2 == 42);
// Note that a needs to be iniliazed because shuffled_args_check takes a const ref
int a = 0;
shuffled_args_check<int>(a);
shuffled_args_check<double>(a);
// shuffled_args_check<int, double>(a); // "Duplicate argument: ..."
// shuffled_args_check<double>(a, a); // "Ambiguous argument: ..."
// shuffled_args_check<char*>(a); // "Incompatible argument: ..."
auto t2 = shuffled_tuple<int, ::std::string>("hello", 1);
auto [x, str] = shuffled_tuple<int, ::std::string>("hello", 1);
EXPECT(str == "hello");
EXPECT(x == 1);
struct A
{
int value;
};
struct B
{
::std::string text;
};
struct C
{
float number;
};
A aa{10};
B bb{"example"};
C cc{3.14f};
// This will create an `::std::tuple<A, B, C>` from the provided arguments,
// automatically assigning them based on their types.
shuffled_tuple<A, B, C>(bb, cc, aa);
shuffled_tuple<A, B, C>(cc, aa);
shuffled_tuple<A, B, C>();
// This will not compile because there are two arguments that can fill the same tuple slot.
// auto my_tuple = shuffled_tuple<A, B, C, double>(bb, cc, aa, aa);
// This will not compile because the argument '5' can convert to both 'int' and 'float',
// causing an ambiguity.
// auto my_tuple = shuffled_tuple<A, B, C>(b, c, 5);
};
UNITTEST("shuffled_array_tuple")
{
using namespace cuda::experimental::stf;
auto a = shuffled_array_tuple<int, ::std::string>(1, 2, "hello", 3, "world");
EXPECT((a == ::std::tuple<::std::array<int, 3>, ::std::array<::std::string, 2>>{{1, 2, 3}, {"hello", "world"}}));
};
UNITTEST("cuda::std::source_location")
{
auto test_func = [](const _CUDA_VSTD::source_location loc = _CUDA_VSTD::source_location::current()) {
// Check the source location metadata
EXPECT(loc.line() > 0); // The line number should be positive
EXPECT(loc.file_name() != nullptr); // File name should not be null
EXPECT(loc.function_name() != nullptr); // Function name should not be null
};
// Call the test function and validate the source location information
test_func();
};
#else // DOXYGEN_SHOULD_SKIP_THIS Do not document
// Ensure these are ignored by Doxygen
# define UNITTEST(name, ...)
#endif // DOXYGEN_SHOULD_SKIP_THIS Do not document