libcudf 中的单元测试使用 Google Test 编写。
重要提示: 请勿直接包含 gtest/gtest.h
,而应使用 #include <cudf_test/cudf_gtest.hpp>
。
此外,请在全局命名空间中编写测试代码。也就是说,请勿在 cudf
或 cudf::test
命名空间或其子命名空间中编写测试代码。同样,请勿在全局命名空间中使用 using namespace cudf;
或 using namespace cudf::test;
。
一般来说,我们应该确保所有代码路径都得到覆盖。这并非总是容易或可能的。但通常这意味着我们测试所有支持的算法和数据类型的组合,以及支持多种操作的算法(例如 reductions, groupby)支持的所有操作符。以下是一些其他指南。
é
。offset
的列)。这是一个容易遗漏的情况。cudf::test::NumericTypes
类型列表中,但包含在 cudf::test::FixedWidthTypes
中,因此请注意测试应酌情包含或排除 decimal types。单元测试目录和源文件的命名应与被测试的功能一致。例如,copying.hpp
中 API 的测试应该位于 cudf/cpp/tests/copying
。每个功能(或一组相关功能)应该有自己的测试源文件,命名为 <feature>_tests.cu/cpp
。例如,cudf/cpp/src/copying/scatter.cu
的测试位于 cudf/cpp/tests/copying/scatter_tests.cu
。
为了缩短编译时间,只要可能,测试源文件应为 .cpp
文件,因为 nvcc
在编译 host code 时比 gcc
慢。请注意,thrust::device_vector
包含 device code,因此只能在 .cu
文件中使用。rmm::device_uvector
、rmm::device_buffer
以及后面描述的各种 column_wrapper
类型可在 .cpp
文件中使用,因此在测试代码中优先于 thrust::device_vector
。
所有 libcudf 单元测试都应使用 GTest "Test Fixture"。即使 fixture 为空,它也应继承自位于 include/cudf_test/base_fixture.hpp
中的基本 fixture cudf::test::BaseFixture
。这确保了 RMM 被正确初始化和最终化。cudf::test::BaseFixture
已经继承自 testing::Test
,因此您的测试 fixtures 无需再继承自它。
示例
class MyTestFixture : public cudf::test::BaseFixture {...};
一般来说,libcudf 的功能必须支持所有受支持的类型(例如,并非所有二元操作都支持所有类型,存在例外)。为了自动化在多种类型上运行相同测试的过程,我们使用 GTest 的 类型化测试 (Typed Tests)。类型化测试允许您编写一个测试,然后在类型列表上运行它。
例如
要指定使用的类型列表,libcudf 不使用 GTest 的 testing::Types<...>
,而是提供 cudf::test::Types<...>
,这是一个自定义的 testing::Types
的直接替代品。在此示例中,所有使用 TypedTestFixture
fixture 的测试将针对 TestTypes
(int, float, double
) 中定义的列表中的每种类型运行一次。
测试中使用的类型列表应在所有测试中保持一致。为了确保一致性,include/cudf_test/type_lists.hpp
中提供了几组常用的类型列表。例如,cudf::test::NumericTypes
是所有数值类型的类型列表,FixedWidthTypes
是所有固定宽度元素类型的列表,cudf::test::AllTypes
是 libcudf 支持的每种元素类型的列表。
只要可能,请使用 include/utilities/test/type_lists.hpp
中提供的类型列表之一,而不是创建新的自定义列表。
有时需要生成比上面 TypeList
示例中简单的单类型列表更高级的类型列表。libcudf 在 include/cudf_test/type_list_utilities.hpp
中提供了一组元编程工具,用于生成和组合更高级的类型列表。
例如,生成一个*嵌套*类型列表可能很有用,其中列表中的每个元素都是两种类型。在嵌套类型列表中,列表中的每个元素本身都是另一个列表。要访问嵌套列表中的第 N
种类型,请使用 GetType<NestedList, N>
。
假设测试 <int,float>
的所有可能的两种类型组合。这可以手动完成
上面的示例手动指定了由 int
和 float
组成的所有对。CrossProduct
是 type_list_utilities.hpp
中的一个工具,它自动实现这种笛卡尔积。
CrossProduct
可以与任意数量的类型列表一起使用,以生成由两种或更多类型组成的嵌套类型列表。**然而**,过度使用 CrossProduct
会显着增加编译时间。两个大小分别为 n
和 m
的类型列表的笛卡尔积将生成一个包含 n*m
个嵌套类型列表的新列表。这意味着将实例化 n*m
个模板;即使 n
和 m
不大,编译时间也可能变得不合理。
type_list_utilities.hpp
中还有许多其他实用工具。更多详细信息,请参阅该文件中的文档以及 cudf/cpp/tests/utilities_tests/type_list_tests.cpp
中的相关测试。
libcudf 在 include/cudf_test
中提供了许多实用工具,以方便进行常见的测试操作。在创建自己的测试实用工具之前,请查看是否已存在一个可以满足您需求的工具。如果不存在,请考虑添加一个新的实用工具来满足您的需求。但是,请确保该实用工具足够通用,对其他测试也有用,并且不要过度针对您的特定测试需求进行定制。
为了更容易生成输入列,libcudf 在 include/cudf_test/column_wrapper.hpp
中提供了 *_column_wrapper
类。这些类包装了一个 cudf::column
,并提供了构造函数,用于初始化可与 libcudf API 一起使用的 cudf::column
对象。任何 *_column_wrapper
类都可以隐式转换为 column_view
或 mutable_column_view
,因此可以透明地传递给任何期望 column_view
或 mutable_column_view
参数的 API。
cudf::test::fixed_width_column_wrapper
类应用于构造和初始化任何固定宽度元素类型的列,例如数值类型、时间戳类型、布尔类型等。cudf::test::fixed_width_column_wrapper
提供了接受迭代器范围的构造函数,用于生成列中的每个元素。对于可空列,可以提供一个额外的迭代器来指示每个元素的有效性。还有构造函数接受 std::initializer_list<T>
用于列元素以及可选地用于每个元素的有效性。
示例
cudf::test::fixed_point_column_wrapper
类应用于构造和初始化任何定点元素类型(DECIMAL32 或 DECIMAL64)的列。cudf::test::fixed_point_column_wrapper
提供了接受迭代器范围的构造函数,用于生成列中的每个元素。对于可空列,可以提供一个额外的迭代器来指示每个元素的有效性。构造函数还接受要创建的定点值的比例 (scale)。
示例
cudf::test::dictionary_column_wrapper
类应用于创建字典列。cudf::test::dictionary_column_wrapper
提供了接受迭代器范围的构造函数,用于生成列中的每个元素。对于可空列,可以提供一个额外的迭代器来指示每个元素的有效性。还有构造函数接受 std::initializer_list<T>
用于列元素以及可选地用于每个元素的有效性。
示例
cudf::test::strings_column_wrapper
类应用于创建 string 列。它提供了接受迭代器范围的构造函数,用于生成列中的每个 string。对于可空列,可以提供一个额外的迭代器来指示每个 string 的有效性。还有构造函数接受 std::initializer_list<std::string>
用于列的 strings 以及可选地用于每个元素的有效性。
示例
cudf::test::lists_column_wrapper
类应用于创建 list 列。它提供了接受迭代器范围的构造函数,用于生成列中的每个 list。对于可空列,可以提供一个额外的迭代器来指示每个 list 的有效性。还有构造函数接受 std::initializer_list<T>
用于列的 lists 以及可选地用于每个元素的有效性。还提供了许多其他构造函数。
示例
cudf::test::structs_column_wrapper
类应用于创建 struct 列。它提供了接受预构建的子列或子列 column wrappers 的 vector 或 initializer list 的构造函数。对于可空列,可以提供一个额外的迭代器来指示每个 struct 的有效性。
示例
测试中的一项常见操作是验证两列是否相等、等价,或者它们是否具有相同的元数据。
验证两列是否具有相同的类型、大小和可空性。对于嵌套类型,递归验证所有嵌套子列的类型、大小和可空性是否相等。
验证两列是否具有等价的类型和相等的大小,忽略可空性。对于嵌套类型,递归验证所有嵌套子列的类型是否等价以及大小是否相等,忽略可空性。
注意“等价类型”。大多数类型当且仅当它们相等时才等价。fixed_point
类型是一个例外。如果它们的表示类型相等,即使比例 (scales) 不同,它们也是等价的。嵌套类型列在大小都为零但一个有子列(也为空)而另一个没有子列的情况下可以等价。对于大小非零的列,相等和等价都要求子列数量相等。
验证两列属性相等,并验证列数据的逐元素相等性。Null 元素被视为相等。
验证两列属性等价,并验证列数据的逐元素等价性。Null 元素被视为等价。
验证两个 device memory buffers 的按位相等性。
不应直接使用 cudf::test::detail
命名空间中的列比较函数。
头文件 <cudf_test/debug_utilities.hpp>
定义了各种函数和重载,用于打印列 (print
)、将列数据转换为 string (to_string
, to_strings
) 以及将数据复制到 host (to_host
)。例如,要将 cudf::column_view
的内容或 column_wrapper
实例打印到控制台,请使用 cudf::test::print()
固定宽度和 string 列以逗号分隔的条目形式输出,包括 null 行。嵌套列也支持,输出包含 offsets 和数据子列以及 null mask 位。
libcudf 使用定制的 preload library 来验证其内部流使用情况(代码可以在 此处
找到)。该库会包装每个接受流的异步 CUDA 运行时 API 调用,进行检查以确保传递的 CUDA 流是有效的,如果检测到无效流会立即抛出异常。加载此库运行测试会立即触发错误,如果任何测试意外地在无效流上运行代码。
流的有效性通过重载 libcudf 默认流的定义来确定。通常,在 libcudf 中,cudf::get_default_stream
返回 rmm
的默认流值之一(取决于 libcudf 是否启用了 per thread default stream 编译)。在 preload library 中,此函数被重新定义,转而返回一个使用函数局部静态 rmm::cuda_stream
管理的新创建的用户流。在这种情况下,无效流被定义为 CUDA 的任何默认流值(cudaStreamLegacy, cudaStreamDefault 或 cudaStreamPerThread),因为任何正确使用 cudf::get_default_stream
的 kernel 现在将转而使用 preload library 创建的自定义流。
preload library 支持两种不同的模式:cudf
模式和 testing
模式。上一段描述了 cudf
模式的行为,其中 cudf::get_default_stream
被重载。在 cudf
模式下,preload library 确保所有 CUDA 运行时 API 都被提供了 cudf 的默认流。这将检测到疏忽之处,例如 Thrust 调用未指定流,或者显式指定了 CUDA 默认流值之一给 kernel。然而,它不会检测到流未在调用堆栈中正确传递的情况,例如,如果某个接受流参数的 detail
函数未能将其传递下去,而是错误地调用了 cudf::get_default_stream
。
在 testing
模式下,库会转而重载 cudf::test::get_default_stream
。此函数定义在 cudf::test
命名空间中,启用了一种更严格的测试模式。在 testing
模式下,preload library 会验证所有 CUDA 运行时 API 是否都使用测试命名空间的默认流进行调用。这种区别很重要,因为 cudf 内部从不使用 cudf::test::get_default_stream
,因此这个流值只有在提供给公共 API 并沿调用堆栈正确传递的情况下才可能在内部出现。虽然 testing
模式比 cudf
模式更严格,但它也更具侵入性。cudf
模式无需更改库或测试即可运行,因为 preload library 会原地覆盖相关 API。然而,testing
模式只能用于验证正确将 cudf::test::get_default_stream
传递给公共 libcudf API 的测试。
除了 preload library,测试套件还实现了一个 custom memory resource,当其 do_allocate
方法被调用时,它会执行类似的流验证。在测试期间,rmm 的默认内存资源被设置为使用此 adaptor 进行额外的流验证。
在为 libcudf API 编写测试时,应添加一组特殊的额外测试来验证 API 的流使用情况。这些测试应放置在 cpp/tests/streams
目录中,文件应对应于包含被测试 API 的头文件,例如,对于 cpp/include/cudf/copying.hpp
中声明的所有 API,测试文件为 cpp/tests/streams/copying_test.cpp
。这些测试应包含对被测试 API 的最小化调用,不带任何额外断言,因为它们仅用于检查流使用情况。将这些测试添加到 cpp/tests/CMakeLists.txt
时,应向 ConfigureTest
CMake 函数提供参数 STREAM_MODE testing
。此更改足以让 CTest 设置测试在运行时自动加载在 testing
模式下编译的 preload library。
测试套件的其余部分配置为在 cudf
模式下使用 preload library 运行。因此,所有使用 ctest
运行的测试都将包含流验证。由于此配置通过 CMake 和 CTest 管理,直接执行测试可执行文件将完全不使用 preload library。然而,在这种情况下,测试仍将正常运行并通过(preload library 本身测试除外)。