diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 053f6385acb84363882f529655a68db0a6473ade..b60d2dba6576941c261635b53bc402881e3b6118 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -14,7 +14,11 @@ set(INTRINSICCV_WARNING_FLAGS "-Wold-style-cast" ) -set(INTRINSICCV_TEST_MAIN ${CMAKE_CURRENT_SOURCE_DIR}/framework/test_main.cpp) +set(INTRINSICCV_TEST_FRAMEWORK_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/framework/border.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/framework/test_main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/framework/utils.cpp +) include(FetchContent) FetchContent_Declare( diff --git a/test/api/CMakeLists.txt b/test/api/CMakeLists.txt index b76740d8cfd4ce48596f370c681437a73e754e5c..5cc3a47b91741490ebfcabad1b0ddb59308da162 100644 --- a/test/api/CMakeLists.txt +++ b/test/api/CMakeLists.txt @@ -2,9 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 -file(GLOB intrinsiccv_api_test_sources CONFIGURE_DEPENDS "*.h" "*.cpp") +file(GLOB intrinsiccv_api_test_sources CONFIGURE_DEPENDS "*.h" "test_*.cpp") -list(APPEND intrinsiccv_api_test_sources ${INTRINSICCV_TEST_MAIN}) +list(APPEND intrinsiccv_api_test_sources ${INTRINSICCV_TEST_FRAMEWORK_SOURCES}) set_source_files_properties( ${intrinsiccv_api_test_sources} diff --git a/test/api/test_sobel.cpp b/test/api/test_sobel.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0c5d8a7c16a4f0063cc5145a5fa315d1236a5ec2 --- /dev/null +++ b/test/api/test_sobel.cpp @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include + +#include "framework/array.h" +#include "framework/generator.h" +#include "framework/kernel.h" + +#define INTRINSICCV_SOBEL_3X3_HORIZONTAL(type, suffix) \ + INTRINSICCV_API(sobel_3x3_horizontal, \ + intrinsiccv_sobel_3x3_horizontal_##suffix, type) + +#define INTRINSICCV_SOBEL_3X3_VERTICAL(type, suffix) \ + INTRINSICCV_API(sobel_3x3_vertical, intrinsiccv_sobel_3x3_vertical_##suffix, \ + type) + +INTRINSICCV_SOBEL_3X3_HORIZONTAL(uint8_t, s16_u8); + +INTRINSICCV_SOBEL_3X3_VERTICAL(uint8_t, s16_u8); + +/// Implements KernelTestParams for Sobel operators. +template +struct SobelKernelTestParams; + +template +struct SobelKernelTestParams { + using InputType = uint8_t; + using IntermediateType = int16_t; + using OutputType = int16_t; + + static constexpr bool kIsHorizontal = IsHorizontal; +}; // end of struct SobelKernelTestParams + +static constexpr std::array kSupportedBorders = { + INTRINSICCV_BORDER_TYPE_REPLICATE, +}; + +/// Default test fo horizontal Sobel 3x3 operator. +template +class Sobel3x3Test : public test::KernelTest { + using typename test::KernelTest::InputType; + using typename test::KernelTest::IntermediateType; + using typename test::KernelTest::OutputType; + + void call_api(const test::Array2D *input, + test::Array2D *output, + intrinsiccv_border_type_t) override { + auto api = KernelTestParams::kIsHorizontal + ? sobel_3x3_horizontal() + : sobel_3x3_vertical(); + api(input->data(), input->stride(), output->data(), output->stride(), + input->width() / input->channels(), input->height(), input->channels()); + } + + public: + void test(test::Array2D mask) { + test::Kernel kernel{mask}; + // Use the default array layouts for testing. + auto array_layouts = + test::default_array_layouts(mask.width(), mask.height()); + // Create generators and execute test. + test::SequenceGenerator tested_borders{kSupportedBorders}; + test::SequenceGenerator tested_array_layouts{array_layouts}; + test::PseudoRandomNumberGenerator element_generator; + this->test::KernelTest::test( + kernel, &tested_array_layouts, &tested_borders, &element_generator); + } +}; // end of class class Sobel3x3Test + +template +class Sobel : public testing::Test {}; + +using ElementTypes = ::testing::Types; +TYPED_TEST_SUITE(Sobel, ElementTypes); + +/// Tests sobel_3x3_horizontal__ API. +TYPED_TEST(Sobel, Horizontal3x3) { + using KernelTestParams = SobelKernelTestParams; + // Horizontal 3x3 Sobel operator. + test::Array2D mask{3, 3}; + mask.set(0, 0, {-1, 0, 1}); + mask.set(1, 0, {-2, 0, 2}); + mask.set(2, 0, {-1, 0, 1}); + Sobel3x3Test{}.test(mask); +} + +/// Tests sobel_3x3_vertical__ API. +TYPED_TEST(Sobel, Vertical3x3) { + using KernelTestParams = SobelKernelTestParams; + // Horizontal 3x3 Sobel operator. + test::Array2D mask{3, 3}; + // clang-format off + mask.set(0, 0, {-1, -2, -1}); + mask.set(1, 0, { 0, 0, 0}); + mask.set(2, 0, { 1, 2, 1}); + // clang-format on + Sobel3x3Test{}.test(mask); +} diff --git a/test/framework/CMakeLists.txt b/test/framework/CMakeLists.txt index 1c5c9eb9d08611cfda724b9e3d8d9271b622c767..1a0633031b8b2595b53203c7a427ee2b050e1d1c 100644 --- a/test/framework/CMakeLists.txt +++ b/test/framework/CMakeLists.txt @@ -2,7 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 -file(GLOB intrinsiccv_framework_test_sources CONFIGURE_DEPENDS "*.h" "*.cpp") +file(GLOB intrinsiccv_framework_test_sources CONFIGURE_DEPENDS "*.h" "test_*.cpp") + +list(APPEND intrinsiccv_framework_test_sources ${INTRINSICCV_TEST_FRAMEWORK_SOURCES}) set_source_files_properties( ${intrinsiccv_framework_test_sources} diff --git a/test/framework/abstract.h b/test/framework/abstract.h new file mode 100644 index 0000000000000000000000000000000000000000..15156e153117c69fb383900919c0b08541c05521 --- /dev/null +++ b/test/framework/abstract.h @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef INTRINSICCV_TEST_FRAMEWORK_ABSTRACT_H_ +#define INTRINSICCV_TEST_FRAMEWORK_ABSTRACT_H_ + +#include +#include + +namespace test { + +/// Interface for objects which represent a finite two-dimensional array. +template +class TwoDimensional { + public: + virtual ~TwoDimensional() = default; + + /// Returns the number of elements in a row of a two-dimensional array. + virtual size_t width() const = 0; + + /// Returns the number of rows of a two-dimensional array. + virtual size_t height() const = 0; + + /// Returns the number of channels. + virtual size_t channels() const { return 1ul; } + + /// Returns a pointer to a data element at a given row and column position, or + /// nullptr if the requested position is invalid. + virtual ElementType *at(size_t row, size_t column) = 0; + + /// Returns a const pointer to a data element at a given row and column + /// position, or nullptr if the requested position is invalid. + virtual const ElementType *at(size_t row, size_t column) const = 0; +}; // end of class TwoDimensional + +/// Interface for objects which have a border. +class Bordered { + public: + virtual ~Bordered() = default; + + /// Returns left border width. + virtual size_t left() const = 0; + + /// Returns top border height. + virtual size_t top() const = 0; + + /// Returns right border width. + virtual size_t right() const = 0; + + /// Returns bottom border height. + virtual size_t bottom() const = 0; +}; // end of class Bordered + +/// Interface for objects which generate some values. +template +class Generator { + public: + virtual ~Generator() = default; + + /// Resets the generator to its initial state. + virtual void reset() {} + + /// Yields the next value or std::nullopt. + virtual std::optional next() = 0; +}; // end of class Generator + +} // namespace test + +#endif // INTRINSICCV_TEST_FRAMEWORK_ABSTRACT_H_ diff --git a/test/framework/array.h b/test/framework/array.h index ff5ce77d0d8a250d79bd9f778f54633717f9ce37..2cb2a2efa5a3b11eafd983f552d917ace273e6d9 100644 --- a/test/framework/array.h +++ b/test/framework/array.h @@ -16,27 +16,32 @@ #include #include +#include "framework/abstract.h" +#include "framework/types.h" +#include "framework/utils.h" + namespace test { /// A simple two-dimensional array representation. template -class Array2D final { +class Array2D : public TwoDimensional { public: Array2D() = default; explicit Array2D(size_t width, size_t height) : Array2D(width, height, 0) {} - explicit Array2D(size_t width, size_t height, size_t padding_bytes) + explicit Array2D(size_t width, size_t height, size_t padding) + : Array2D(width, height, padding, 1) {} + + explicit Array2D(ArrayLayout layout) + : Array2D(layout.width, layout.height, layout.padding, layout.channels) {} + + explicit Array2D(size_t width, size_t height, size_t padding, size_t channels) : width_{width}, height_{height}, - stride_{width * sizeof(ElementType) + padding_bytes} { - try { - data_ = std::make_unique(height_ * stride_); - } catch (...) { - width_ = height_ = stride_ = 0; - return; - } - + channels_{channels}, + stride_{width * sizeof(ElementType) + padding} { + try_allocate(); fill_padding(); } @@ -44,27 +49,44 @@ class Array2D final { ~Array2D() { check_padding(); } /// Copy constructor. - Array2D(const Array2D &other) = delete; + Array2D(const Array2D &other) { this->operator=(other); } /// Move constructor. - Array2D(Array2D &&other) = delete; + Array2D(Array2D &&other) { this->operator=(other); } /// Copy assignment operator. - Array2D &operator=(const Array2D &other) = delete; + Array2D &operator=(const Array2D &other) { + width_ = other.width_; + height_ = other.height_; + channels_ = other.channels_; + stride_ = other.stride_; + + try_allocate(); + + EXPECT_TRUE(valid()); + if (valid()) { + std::memcpy(data_.get(), other.data_.get(), height_ * stride_); + } + + return *this; + } /// Move assignment operator. Array2D &operator=(Array2D &&other) { data_ = std::move(other.data_); width_ = other.width_; height_ = other.height_; + channels_ = other.channels_; stride_ = other.stride_; - other.width_ = other.height_ = other.stride_ = 0; + other.reset(); return *this; } /// Fills the underlying memory range with a given value skipping padding /// bytes. void fill(ElementType value) { + ASSERT_EQ(valid(), true); + ElementType *ptr = data(); for (size_t row = 0; row < height(); ++row) { for (size_t column = 0; column < width(); ++column) { @@ -75,10 +97,30 @@ class Array2D final { } } - /// Sets values starting in a given row starting at a given column. Returns - /// false if there is not enough space in the row. + /// Fills the underlying memory range with a given generator skipping padding + /// bytes. + void fill(Generator *generator) { + ASSERT_NE(generator, nullptr); + ASSERT_EQ(valid(), true); + + ElementType *ptr = data(); + for (size_t row = 0; row < height(); ++row) { + for (size_t column = 0; column < width(); ++column) { + std::optional optional_value = generator->next(); + ASSERT_NE(optional_value, std::nullopt); + ptr[column] = optional_value.value(); + } + + ptr = add_stride(ptr, 1); + } + } + + /// Sets values in a row starting at a given column. void set(size_t row, size_t column, std::initializer_list values) { + ASSERT_EQ(valid(), true) << "Array is invalid."; + ASSERT_GE(width() - column, values.size()); + ElementType *ptr = at(row, column); if (!ptr) { return; @@ -90,6 +132,25 @@ class Array2D final { } } + /// Sets values starting in a given row starting at a given column. + /// + /// The layout of the input TwoDimensional object is not altered, meaning that + /// it must fit as-is at the given row and column position into the array. + void set(size_t row, size_t column, + const TwoDimensional *elements) { + ASSERT_NE(elements, nullptr); + ASSERT_GE(width(), column); + ASSERT_GE(height(), row); + ASSERT_GE(width() - column, elements->width()); + ASSERT_GE(height() - row, elements->height()); + + for (size_t row_offset = 0; row_offset < elements->height(); ++row_offset) { + const ElementType *src = elements->at(row_offset, 0); + ElementType *dst = at(row + row_offset, column); + std::memcpy(dst, src, elements->width() * sizeof(ElementType)); + } + } + /// Compares two instances for equality considering only element bytes. /// Returns the location of the first mismatch, if any. std::optional> compare_to( @@ -98,7 +159,7 @@ class Array2D final { for (size_t column = 0; column < width(); ++column) { const ElementType *lhs = at(row, column); const ElementType *rhs = other.at(row, column); - if (!lhs || !rhs || lhs[0] != rhs[0]) { + if (!lhs || !rhs || (lhs[0] != rhs[0])) { return std::make_tuple(row, column); } } @@ -116,29 +177,33 @@ class Array2D final { } /// Returns the width of this array. - size_t width() const { return width_; } + size_t width() const override { return width_; } /// Returns the height of this array. - size_t height() const { return height_; } + size_t height() const override { return height_; } + + /// Returns the number of channels. + size_t channels() const override { return channels_; }; /// Returns the stride of this array. size_t stride() const { return stride_; } - /// Returns true if this object hold actual memory, otherwise false. + /// Returns true if this object holds actual memory, otherwise false. bool valid() const { return data() != nullptr; } /// Returns a pointer to a data element at a given row and column position, or /// nullptr if the requested position is invalid. - ElementType *at(size_t row, size_t column) { + ElementType *at(size_t row, size_t column) override { return const_cast( const_cast *>(this)->at(row, column)); } - /// Returns a constant pointer to a data element at a given row and column + /// Returns a const pointer to a data element at a given row and column /// position, or nullptr if the requested position is invalid. - const ElementType *at(size_t row, size_t column) const { + const ElementType *at(size_t row, size_t column) const override { if (!check_access(row, column)) { - return nullptr; + TEST_FAIL_WITH(nullptr, + "Access is either out-of-bounds or the array is invalid."); } const ElementType *ptr = add_stride(data(), row); @@ -146,7 +211,7 @@ class Array2D final { } private: - /// Returns the offset to the padding within a row. + /// Returns the offset to the first padding byte within a row. size_t padding_offset() const { return width() * sizeof(ElementType); } /// Returns true if a row has padding, otherwise false. @@ -173,7 +238,7 @@ class Array2D final { } } - /// Checks for clobbered padding, if present. + /// Checks for clobbered padding bytes, if present. void check_padding() const { if (!valid() || !has_padding()) { return; @@ -183,9 +248,8 @@ class Array2D final { for (size_t row = 0; row < height(); ++row) { for (size_t offset = padding_offset(); offset < stride(); ++offset) { if (ptr[offset] != kPaddingValue) { - FAIL() << "Padding byte was overwritten at (row=" << row - << ", offset=" << offset << ")" << std::endl; - return; + GTEST_FAIL() << "Padding byte was overwritten at (row=" << row + << ", offset=" << offset << ")" << std::endl; } } @@ -195,18 +259,37 @@ class Array2D final { /// Adds stride to a pointer. ElementType *add_stride(ElementType *ptr, size_t count) { - size_t address = reinterpret_cast(ptr); + char *address = reinterpret_cast(ptr); address += count * stride(); return reinterpret_cast(address); } /// Adds stride to a pointer. const ElementType *add_stride(const ElementType *ptr, size_t count) const { - size_t address = reinterpret_cast(ptr); + const char *address = reinterpret_cast(ptr); address += count * stride(); return reinterpret_cast(address); } + /// Resets the instance to the default instance. + void reset() { + width_ = height_ = channels_ = stride_ = 0; + data_.reset(); + } + + /// Tries to allocate backing memory. + void try_allocate() { + size_t allocation_size = height_ * stride_; + + try { + data_ = std::make_unique(allocation_size); + } catch (...) { + reset(); + GTEST_FAIL() << "Failed to allocate memory of " << allocation_size + << " bytes"; + } + } + /// Constant value of row padding bytes. static constexpr uint8_t kPaddingValue = std::numeric_limits::max(); @@ -216,37 +299,44 @@ class Array2D final { size_t width_{0}; /// Number of rows in the array. size_t height_{0}; + /// Number of channels. + size_t channels_{0}; /// Stride in bytes between the first elements of two consecutive rows. size_t stride_{0}; }; // end of class Array2D /// Compares two \ref Array2D objects for equality. -#define EXPECT_EQ_ARRAY2D(__lhs, __rhs) \ - do { \ - ASSERT_EQ((__lhs).width(), (__rhs).width()) \ - << "Mismatch in width." << std::endl; \ - ASSERT_EQ((__lhs).height(), (__rhs).height()) \ - << "Mismatch in height." << std::endl; \ - auto mismatch = (__lhs).compare_to((__rhs)); \ - if (mismatch) { \ - auto [row, col] = *mismatch; \ - FAIL() << "Mismatch at (row=" << row << ", col=" << col \ - << "): " << (__lhs).at(row, col)[0] << " vs " \ - << (__rhs).at(row, col)[0] << "." << std::endl; \ - } \ +#define EXPECT_EQ_ARRAY2D(lhs, rhs) \ + do { \ + ASSERT_EQ((lhs).width(), (rhs).width()) \ + << "Mismatch in width." << std::endl; \ + ASSERT_EQ((lhs).height(), (rhs).height()) \ + << "Mismatch in height." << std::endl; \ + ASSERT_EQ((lhs).channels(), (rhs).channels()) \ + << "Mismatch in channels." << std::endl; \ + auto mismatch = (lhs).compare_to((rhs)); \ + if (mismatch) { \ + auto [row, col] = *mismatch; \ + GTEST_FAIL() << "Mismatch at (row=" << row << ", col=" << col \ + << "): " << (lhs).at(row, col)[0] << " vs " \ + << (rhs).at(row, col)[0] << "." << std::endl; \ + } \ } while (0 != 0) /// Compares two \ref Array2D objects for inequality. -#define EXPECT_NE_ARRAY2D(__lhs, __rhs) \ - do { \ - ASSERT_EQ((__lhs).width(), (__rhs).width()) \ - << "Mismatch in width." << std::endl; \ - ASSERT_EQ((__lhs).height(), (__rhs).height()) \ - << "Mismatch in height." << std::endl; \ - auto mismatch = (__lhs).compare_to((__rhs)); \ - if (!mismatch) { \ - FAIL() << "Objects are equal, but expected to differ." << std::endl; \ - } \ +#define EXPECT_NE_ARRAY2D(lhs, rhs) \ + do { \ + ASSERT_EQ((lhs).width(), (rhs).width()) \ + << "Mismatch in width." << std::endl; \ + ASSERT_EQ((lhs).height(), (rhs).height()) \ + << "Mismatch in height." << std::endl; \ + ASSERT_EQ((lhs).channels(), (rhs).channels()) \ + << "Mismatch in channels." << std::endl; \ + auto mismatch = (lhs).compare_to((rhs)); \ + if (!mismatch) { \ + GTEST_FAIL() << "Objects are equal, but expected to differ." \ + << std::endl; \ + } \ } while (0 != 0) } // namespace test diff --git a/test/framework/border.cpp b/test/framework/border.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a30e1737f4afa8d1a58c85ae68b0890a718cd8c8 --- /dev/null +++ b/test/framework/border.cpp @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#include "framework/border.h" + +#include + +#include + +namespace test { + +/// Creates replicated border elements. +/// +/// Replicating means that the elements at the edges are copied to bordering +/// element positions. For example: +/// | left border | elements | right border | +/// | A A | A B C D E | E E | +template +static void replicate(const Bordered *bordered, + TwoDimensional *elements) { + ASSERT_LE((bordered->left() + bordered->right()) * elements->channels(), + elements->width()); + ASSERT_LE(bordered->top() + bordered->bottom(), elements->height()); + + // Replicate left and right border columns. + for (size_t row = 0; row < elements->height(); ++row) { + for (size_t channel = 0; channel < elements->channels(); ++channel) { + // Prepare left border columns. + for (size_t column = 0; column < bordered->left(); ++column) { + size_t src_column = bordered->left() * elements->channels() + channel; + size_t dst_column = column * elements->channels() + channel; + elements->at(row, dst_column)[0] = elements->at(row, src_column)[0]; + } + + // Prepare right border columns. + for (size_t column = 0; column < bordered->right(); ++column) { + size_t src_column = elements->width() - + (bordered->right() + 1) * elements->channels() + + channel; + size_t dst_column = src_column + (1 + column) * elements->channels(); + elements->at(row, dst_column)[0] = elements->at(row, src_column)[0]; + } + } + } + + // Replicate top border rows. + size_t replicated_top_row = bordered->top(); + for (size_t row = 0; row < bordered->top(); ++row) { + auto row_ptr = elements->at(replicated_top_row, 0); + std::copy(row_ptr, row_ptr + elements->width(), elements->at(row, 0)); + } + + // Replicate bottom border rows. + size_t replicated_bottom_row = elements->height() - bordered->bottom() - 1; + for (size_t row = 0; row < bordered->bottom(); ++row) { + auto row_ptr = elements->at(replicated_bottom_row, 0); + std::copy(row_ptr, row_ptr + elements->width(), + elements->at(replicated_bottom_row + row + 1, 0)); + } +} + +template +void prepare_borders(intrinsiccv_border_type_t border_type, + const Bordered *bordered, + TwoDimensional *elements) { + ASSERT_NE(bordered, nullptr); + ASSERT_NE(elements, nullptr); + + switch (border_type) { + default: + GTEST_FAIL() << "Border type is not implemented."; + + case INTRINSICCV_BORDER_TYPE_REPLICATE: + return replicate(bordered, elements); + } +} + +template void prepare_borders(intrinsiccv_border_type_t, + const Bordered *, + TwoDimensional *); + +} // namespace test diff --git a/test/framework/border.h b/test/framework/border.h new file mode 100644 index 0000000000000000000000000000000000000000..4ed31ab5822892e3b66c90d532bf10cd4da7240e --- /dev/null +++ b/test/framework/border.h @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef INTRINSICCV_TEST_FRAMEWORK_BORDER_H_ +#define INTRINSICCV_TEST_FRAMEWORK_BORDER_H_ + +#include + +#include "framework/abstract.h" + +namespace test { + +/// Prepares bordering elements given a border type and bordering requirements. +template +void prepare_borders(intrinsiccv_border_type_t border_type, + const Bordered *bordered, + TwoDimensional *elements); + +} // namespace test + +#endif // INTRINSICCV_TEST_FRAMEWORK_BORDER_H_ diff --git a/test/framework/generator.h b/test/framework/generator.h new file mode 100644 index 0000000000000000000000000000000000000000..34c56af532b9eaf4dfd1aefc04d8710c0c192b13 --- /dev/null +++ b/test/framework/generator.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef INTRINSICCV_TEST_FRAMEWORK_GENERATOR_H_ +#define INTRINSICCV_TEST_FRAMEWORK_GENERATOR_H_ + +#include + +#include "framework/abstract.h" +#include "framework/utils.h" + +namespace test { + +/// Generates pseudo-random numbers of a given type. +template +class PseudoRandomNumberGenerator : public Generator { + public: + PseudoRandomNumberGenerator() : seed_{Options::seed()} { reset(); } + + /// Resets the generator to the initial state. + void reset() override { rng_.seed(seed_); } + + /// Yields the next value or std::nullopt. + std::optional next() override { + return static_cast(rng_()); + } + + protected: + uint64_t seed_; + std::mt19937_64 rng_; +}; // end of class PseudoRandomNumberGenerator + +/// Generator which yields values of an iterable container. +template +class SequenceGenerator : public Generator { + public: + explicit SequenceGenerator(const IterableType &container) + : begin_{container.begin()}, + current_{container.begin()}, + end_{container.end()} {} + + /// Resets the generator to its initial state. + void reset() override { current_ = begin_; } + + /// Yields the next value or std::nullopt. + std::optional next() override { + if (current_ == end_) { + return std::nullopt; + } + + return *current_++; + } + + protected: + typename IterableType::const_iterator begin_; + typename IterableType::const_iterator current_; + typename IterableType::const_iterator end_; +}; // end of class SequenceGenerator + +} // namespace test + +#endif // INTRINSICCV_TEST_FRAMEWORK_GENERATOR_H_ diff --git a/test/framework/kernel.h b/test/framework/kernel.h new file mode 100644 index 0000000000000000000000000000000000000000..d88e6305389c1a3f01d2803043b07d9dff4dc2c5 --- /dev/null +++ b/test/framework/kernel.h @@ -0,0 +1,263 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef INTRINSICCV_TEST_FRAMEWORK_KERNEL_H_ +#define INTRINSICCV_TEST_FRAMEWORK_KERNEL_H_ + +#include +#include + +#include +#include + +#include "framework/abstract.h" +#include "framework/array.h" +#include "framework/border.h" +#include "framework/generator.h" +#include "framework/types.h" + +namespace test { + +/// Represents a kernel operator. +template +class Kernel : protected Array2D, public Bordered { + public: + // Make some features of Array2D base class public. + using Array2D::at; + using Array2D::channels; + using Array2D::height; + using Array2D::width; + + explicit Kernel(Array2D mask) + : Array2D(mask), + anchor_{mask.width() / 2, mask.height() / 2} {} + + /// Returns the anchor point of the kernel. + Point anchor() const { return anchor_; } + + /// Returns the number of elements to the left of the anchor point. + size_t left() const override { return anchor().x; } + + /// Returns the number of elements above the anchor point. + size_t top() const override { return anchor().y; } + + /// Returns the number of elements to the right of the anchor point. + size_t right() const override { + return (width() > 0) ? width() - anchor().x - 1 : 0; + } + + /// Returns the number of elements above the anchor point. + size_t bottom() const override { + return (height() > 0) ? height() - anchor().y - 1 : 0; + } + + private: + /// The anchor point of the kernel. + Point anchor_; +}; // end of class Kernel + +/// Abstract class to help implement a kernel operation. +/// +/// Required: +/// - KernelTestParams::InputType: Input type to the operation. +/// - KernelTestParams::IntermediateType: Type which is used during element +/// calculations. +/// - KernelTestParams::OutputType: Output type of the operation. +template +class KernelTest { + public: + using InputType = typename KernelTestParams::InputType; + using IntermediateType = typename KernelTestParams::IntermediateType; + using OutputType = typename KernelTestParams::OutputType; + + KernelTest() : debug_{false} {} + + /// Enables debug mode. + KernelTest &with_debug() { + debug_ = true; + return *this; + } + + void test(Generator> *kernel_generator, + Generator *array_layout_generator, + Generator *border_type_generator, + Generator *element_generator) { + ASSERT_NE(kernel_generator, nullptr); + kernel_generator->reset(); + + std::optional> maybe_kernel; + while ((maybe_kernel = kernel_generator->next()) != std::nullopt) { + test(*maybe_kernel, array_layout_generator, border_type_generator, + element_generator); + ASSERT_NO_FAILURES(); + } + } + + void test(Kernel kernel, + Generator *array_layout_generator, + Generator *border_type_generator, + Generator *element_generator) { + ASSERT_NE(array_layout_generator, nullptr); + array_layout_generator->reset(); + + std::optional maybe_array_layout; + while ((maybe_array_layout = array_layout_generator->next()) != + std::nullopt) { + ArrayLayout array_layout = *maybe_array_layout; + create_arrays(kernel, array_layout); + ASSERT_NO_FAILURES(); + test(kernel, array_layout, border_type_generator, element_generator); + ASSERT_NO_FAILURES(); + } + } + + void test(Kernel kernel, ArrayLayout array_layout, + Generator *border_type_generator, + Generator *element_generator) { + ASSERT_NE(border_type_generator, nullptr); + border_type_generator->reset(); + + std::optional maybe_border_type; + while ((maybe_border_type = border_type_generator->next()) != + std::nullopt) { + test(kernel, array_layout, *maybe_border_type, element_generator); + ASSERT_NO_FAILURES(); + } + } + + void test(Kernel kernel, ArrayLayout array_layout, + intrinsiccv_border_type_t border_type, + Generator *element_generator) { + prepare_source(element_generator); + prepare_expected(kernel, array_layout, border_type); + prepare_actual(); + this->call_api(&input_, &actual_, border_type); + check_results(); + } + + protected: + /// Calls the API-under-test in the appropriate way. + /// + /// The arguments are never nullptr. + virtual void call_api(const Array2D *input, + Array2D *output, + intrinsiccv_border_type_t border_type) = 0; + + /// Calculates the expected output. + virtual void calculate_expected(const Kernel &kernel, + const TwoDimensional &source) { + for (size_t row = 0; row < expected_.height(); ++row) { + for (size_t column = 0; column < expected_.width(); ++column) { + IntermediateType result; + result = calculate_expected_at(kernel, source, row, column); + expected_.at(row, column)[0] = static_cast(result); + } + } + } + + /// Calculates the expected element at a given position. + virtual IntermediateType calculate_expected_at( + const Kernel &kernel, + const TwoDimensional &source, size_t row, size_t column) { + IntermediateType result{0}; + for (size_t height = 0; height < kernel.height(); ++height) { + for (size_t width = 0; width < kernel.width(); ++width) { + IntermediateType coefficient = kernel.at(height, width)[0]; + InputType value = + source.at(row + height, column + width * source.channels())[0]; + result += coefficient * static_cast(value); + } + } + + return result; + } + + /// Creates arrays for a given layout. + void create_arrays(const Kernel &kernel, + const ArrayLayout &array_layout) { + input_ = Array2D{array_layout}; + ASSERT_TRUE(input_.valid()); + + expected_ = Array2D{array_layout}; + ASSERT_TRUE(expected_.valid()); + + actual_ = Array2D{array_layout}; + ASSERT_TRUE(actual_.valid()); + + input_with_borders_ = Array2D{ + array_layout.width + + (kernel.left() + kernel.right()) * array_layout.channels, + array_layout.height + kernel.top() + kernel.bottom(), 0, + array_layout.channels}; + ASSERT_TRUE(input_with_borders_.valid()); + } + + /// Prepares input to the kernel-based operation. + void prepare_source(Generator *element_generator) { + ASSERT_NE(element_generator, nullptr); + element_generator->reset(); + input_.fill(element_generator); + + if (debug_) { + std::cout << "[source]" << std::endl; + dump(&input_); + } + } + + /// Computes expected output of the kernel-based operation. + void prepare_expected(const Kernel &kernel, + const ArrayLayout &array_layout, + intrinsiccv_border_type_t border_type) { + input_with_borders_.set(kernel.anchor().x, + kernel.anchor().y * array_layout.channels, &input_); + + if (debug_) { + std::cout << "[input_with_borders without borders]" << std::endl; + dump(&input_with_borders_); + } + + prepare_borders(border_type, &kernel, &input_with_borders_); + + if (debug_) { + std::cout << "[input_with_borders with borders]" << std::endl; + dump(&input_with_borders_); + } + + calculate_expected(kernel, input_with_borders_); + + if (debug_) { + std::cout << "[expected]" << std::endl; + dump(&expected_); + } + } + + /// Prepares the actual output of the kernel-based operation. + void prepare_actual() { actual_.fill(42); } + + /// Checks that the actual output matches the expectations. + void check_results() { + if (debug_) { + std::cout << "[actual]" << std::endl; + dump(&actual_); + } + + // Check that the actual result matches the expectation. + EXPECT_EQ_ARRAY2D(expected_, actual_); + } + + /// Input operand for the operation. + Array2D input_; + /// Input operand with borders, used to calculate expeected values. + Array2D input_with_borders_; + /// Expected result of the operation. + Array2D expected_; + /// Actual result of the operation. + Array2D actual_; + /// Enables debug mode. + bool debug_; +}; // end of class KernelTest + +} // namespace test + +#endif // INTRINSICCV_TEST_FRAMEWORK_KERNEL_H_ diff --git a/test/framework/test_array2d.cpp b/test/framework/test_array2d.cpp index cb606319feaffac08fb4f9bb8603dcb2c3875b60..7aed9667ad24ea82a3407e7e3a1ac64140d4a24f 100644 --- a/test/framework/test_array2d.cpp +++ b/test/framework/test_array2d.cpp @@ -5,9 +5,11 @@ #include #include +#include #include #include "framework/array.h" +#include "framework/generator.h" #include "framework/utils.h" /// Tests that the default constructor of test::Array2D default constructor @@ -16,6 +18,7 @@ TEST(Array2D, DefaultConstructor) { test::Array2D array; EXPECT_EQ(array.width(), 0); EXPECT_EQ(array.height(), 0); + EXPECT_EQ(array.channels(), 0); EXPECT_EQ(array.stride(), 0); EXPECT_FALSE(array.valid()); } @@ -27,6 +30,7 @@ TEST(Array2D, Constructor) { test::Array2D array_1{width, height}; EXPECT_EQ(array_1.width(), width); EXPECT_EQ(array_1.height(), height); + EXPECT_EQ(array_1.channels(), 1); EXPECT_EQ(array_1.stride(), width * sizeof(uint32_t)); EXPECT_TRUE(array_1.valid()); @@ -34,19 +38,46 @@ TEST(Array2D, Constructor) { EXPECT_EQ_ARRAY2D(array_1, array_2); } -/// Tests that the move assignment operator of test::Array2D works. -TEST(Array2D, MoveAssignment) { +/// Tests that test::Array2D is constructible with test::ArrayLayout. +TEST(Array2D, ConstructFromArrayLayout) { + using ElementType = uint32_t; + + size_t width = 1, height = 2, padding = 3, channels = 4; + test::ArrayLayout layout{width, height, padding, channels}; + test::Array2D array{layout}; + EXPECT_EQ(array.width(), width); + EXPECT_EQ(array.height(), height); + EXPECT_EQ(array.channels(), channels); + EXPECT_EQ(array.stride(), width * sizeof(ElementType) + padding); + EXPECT_TRUE(array.valid()); +} + +/// Tests that the copy assignment operator of test::Array2D works. +TEST(Array2D, CopyAssignment) { size_t width = 5, height = 5; test::Array2D array_1{width, height}; + + test::Array2D array_2 = array_1; + EXPECT_EQ_ARRAY2D(array_1, array_2); + EXPECT_NE(array_1.data(), array_2.data()); +} + +/// Tests that the move assignment operator of test::Array2D works. +TEST(Array2D, MoveAssignment) { + size_t width = 5, height = 5, channels = 3; + test::Array2D array_1{width, height, 0, channels}; test::Array2D array_2; array_2 = std::move(array_1); EXPECT_EQ(array_2.width(), width); EXPECT_EQ(array_2.height(), height); + EXPECT_EQ(array_2.channels(), channels); EXPECT_EQ(array_2.stride(), width * sizeof(uint32_t)); EXPECT_TRUE(array_2.valid()); + EXPECT_EQ(array_1.width(), 0); EXPECT_EQ(array_1.height(), 0); + EXPECT_EQ(array_1.channels(), 0); EXPECT_EQ(array_1.stride(), 0); EXPECT_FALSE(array_1.valid()); } @@ -54,25 +85,16 @@ TEST(Array2D, MoveAssignment) { /// Tests that test::Array2D.at() works for set/get. TEST(Array2D, At) { size_t width = 1, height = 1; - test::Array2D array_1{width, height}; - test::Array2D array_2{width, height}; - - array_1.at(0, 0)[0] = 1; - EXPECT_EQ(array_1.at(0, 0)[0], 1); + test::Array2D array{width, height}; - array_1.at(0, 0)[0] = 2; - EXPECT_EQ(array_1.at(0, 0)[0], 2); + array.at(0, 0)[0] = 1; + EXPECT_EQ(array.at(0, 0)[0], 1); - EXPECT_EQ(array_1.at(0, 1), nullptr); - EXPECT_EQ(array_1.at(1, 0), nullptr); - EXPECT_EQ(array_1.at(1, 1), nullptr); + array.at(0, 0)[0] = 2; + EXPECT_EQ(array.at(0, 0)[0], 2); - // Constant array const test::Array2D const_array{width, height}; - - EXPECT_EQ(const_array.at(0, 1), nullptr); - EXPECT_EQ(const_array.at(0, 1), nullptr); - EXPECT_EQ(const_array.at(0, 1), nullptr); + EXPECT_EQ(const_array.at(0, 0)[0], 0); } /// Tests that test::Array2D.set() works. @@ -94,13 +116,74 @@ TEST(Array2D, Set) { EXPECT_EQ(array_1.at(1, 0)[0], 11); } +/// Tests that test::Array2D.set() works for TwoDimensional instances. +TEST(Array2D, SetWithTwoDimensional) { + using ElementType = uint32_t; + + // array_1: + // 0 1 2 + // 3 4 5 + // 6 7 8 + test::Array2D array_1{3, 3}; + array_1.set(0, 0, {0, 1, 2}); + array_1.set(1, 0, {3, 4, 5}); + array_1.set(2, 0, {6, 7, 8}); + + // array_2: + // 9 9 9 9 9 + // 9 9 9 9 9 + // 9 9 9 9 9 + // 9 9 9 9 9 + // 9 9 9 9 9 + test::Array2D array_2{5, 5}; + array_2.fill(9); + + // Expected array_2: + // 9 9 9 9 9 + // 9 9 0 1 2 + // 9 9 3 4 5 + // 9 9 6 7 8 + // 9 9 9 9 9 + array_2.set(1, 2, &array_1); + + EXPECT_EQ(array_2.at(0, 0)[0], 9); + EXPECT_EQ(array_2.at(0, 1)[0], 9); + EXPECT_EQ(array_2.at(0, 2)[0], 9); + EXPECT_EQ(array_2.at(0, 3)[0], 9); + EXPECT_EQ(array_2.at(0, 4)[0], 9); + + EXPECT_EQ(array_2.at(1, 0)[0], 9); + EXPECT_EQ(array_2.at(1, 1)[0], 9); + EXPECT_EQ(array_2.at(1, 2)[0], 0); + EXPECT_EQ(array_2.at(1, 3)[0], 1); + EXPECT_EQ(array_2.at(1, 4)[0], 2); + + EXPECT_EQ(array_2.at(2, 0)[0], 9); + EXPECT_EQ(array_2.at(2, 1)[0], 9); + EXPECT_EQ(array_2.at(2, 2)[0], 3); + EXPECT_EQ(array_2.at(2, 3)[0], 4); + EXPECT_EQ(array_2.at(2, 4)[0], 5); + + EXPECT_EQ(array_2.at(3, 0)[0], 9); + EXPECT_EQ(array_2.at(3, 1)[0], 9); + EXPECT_EQ(array_2.at(3, 2)[0], 6); + EXPECT_EQ(array_2.at(3, 3)[0], 7); + EXPECT_EQ(array_2.at(3, 4)[0], 8); + + EXPECT_EQ(array_2.at(4, 0)[0], 9); + EXPECT_EQ(array_2.at(4, 1)[0], 9); + EXPECT_EQ(array_2.at(4, 2)[0], 9); + EXPECT_EQ(array_2.at(4, 3)[0], 9); + EXPECT_EQ(array_2.at(4, 4)[0], 9); +} + /// Tests that test::Array2D.fill() works. TEST(Array2D, Fill) { size_t width = 5, height = 2; test::Array2D array_1{width, height}; test::Array2D array_2{width, height}; - array_1.fill(0); + array_1.fill(uint32_t{0}); for (size_t row = 0; row < height; ++row) { for (size_t column = 0; column < width; ++column) { EXPECT_EQ(array_1.at(row, column)[0], 0); @@ -115,6 +198,25 @@ TEST(Array2D, Fill) { } } +/// Tests that test::Array2D.fill(Generator *) works. +TEST(Array2D, FillWithGenerator) { + using ElementType = uint32_t; + + const size_t width = 3, height = 2; + test::Array2D array{width, height}; + + const size_t num_elements = width * height; + std::array elements = {11, 12, 13, 14, 15, 16}; + test::SequenceGenerator generator{elements}; + array.fill(&generator); + EXPECT_EQ(array.at(0, 0)[0], 11); + EXPECT_EQ(array.at(0, 1)[0], 12); + EXPECT_EQ(array.at(0, 2)[0], 13); + EXPECT_EQ(array.at(1, 0)[0], 14); + EXPECT_EQ(array.at(1, 1)[0], 15); + EXPECT_EQ(array.at(1, 2)[0], 16); +} + /// Tests that EXPECT_EQ_ARRAY2D() macro works for fully-equal objects. TEST(Array2D, ExpectEq_Equal) { size_t width = 5, height = 2; @@ -162,6 +264,21 @@ TEST(Array2D, ExpectEq_NotEqual_Height) { EXPECT_FATAL_FAILURE(Test::test(), "Mismatch in height."); } +/// Tests that EXPECT_EQ_ARRAY2D() macro works for non-equal objects where +/// channels is different. +TEST(Array2D, ExpectEq_NotEqual_Channels) { + struct Test { + static void test() { + size_t width = 5, height = 2; + test::Array2D array_1{width, height, 0, 1}; + test::Array2D array_2{width, height, 0, 2}; + EXPECT_EQ_ARRAY2D(array_1, array_2); + } + }; + + EXPECT_FATAL_FAILURE(Test::test(), "Mismatch in channels."); +} + /// Tests that EXPECT_EQ_ARRAY2D() macro works for non-equal objects where data /// is different. TEST(Array2D, ExpectEq_NotEqual_Data) { @@ -223,6 +340,21 @@ TEST(Array2D, ExpectNe_NotEqual_Height) { EXPECT_FATAL_FAILURE(Test::test(), "Mismatch in height."); } +/// Tests that EXPECT_NE_ARRAY2D() macro works for non-equal objects where +/// channels is different. +TEST(Array2D, ExpectNe_NotEqual_Channels) { + struct Test { + static void test() { + size_t width = 5, height = 2; + test::Array2D array_1{width, height, 0, 1}; + test::Array2D array_2{width, height, 0, 2}; + EXPECT_NE_ARRAY2D(array_1, array_2); + } + }; + + EXPECT_FATAL_FAILURE(Test::test(), "Mismatch in channels."); +} + /// Tests that EXPECT_NE_ARRAY2D() macro works for non-equal objects where there /// is a difference in data. TEST(Array2D, ExpectNe_NotEqual_Data) { @@ -268,13 +400,27 @@ TEST(Array2D, PaddingClobbered_LastRow_LastByte) { "Padding byte was overwritten at (row=1, offset=29)"); } -/// Additional tests for coverage purposes. -TEST(Array2D, Coverage) { +static void Array2DCoverageBadAlloc() { test::Array2D array{std::numeric_limits::max(), 1}; EXPECT_FALSE(array.valid()); +} +static void Array2DCoverageSetInvalidArray() { + test::Array2D array; array.set(0, 0, {}); +} + +static void Array2DCoverageAtInvalidArray() { + test::Array2D array; + array.at(0, 0); +} - uint64_t* ptr_1 = array.at(0, 0); - EXPECT_EQ(ptr_1, nullptr); +/// Additional tests for coverage purposes. +TEST(Array2D, Coverage) { + EXPECT_FATAL_FAILURE(Array2DCoverageBadAlloc(), + "Failed to allocate memory of"); + EXPECT_FATAL_FAILURE(Array2DCoverageSetInvalidArray(), "Array is invalid."); + EXPECT_FATAL_FAILURE( + Array2DCoverageAtInvalidArray(), + "Access is either out-of-bounds or the array is invalid."); } diff --git a/test/framework/test_border.cpp b/test/framework/test_border.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ebbf03fc1e9e2e335525c1119649946cf4e0e9d2 --- /dev/null +++ b/test/framework/test_border.cpp @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "framework/abstract.h" +#include "framework/array.h" +#include "framework/border.h" + +class ImplementsBorder : public test::Bordered { + public: + explicit ImplementsBorder(size_t left, size_t top, size_t right, + size_t bottom) + : left_{left}, top_{top}, right_{right}, bottom_{bottom} {} + + size_t left() const override { return left_; }; + size_t top() const override { return top_; }; + size_t right() const override { return right_; }; + size_t bottom() const override { return bottom_; }; + + private: + size_t left_; + size_t top_; + size_t right_; + size_t bottom_; +}; // end of class ImplementsBorder + +/// Tests that replicate border strategy works for one channel and one bordering +/// element in every direction. +TEST(Border, Replicate_1Ch_1Element) { + using ElementType = uint8_t; + + const size_t width = 6, height = 5; + + test::Array2D actual{width, height}; + actual.set(0, 0, {0, 0, 0, 0, 0, 0}); + actual.set(1, 0, {0, 1, 2, 3, 4, 0}); + actual.set(2, 0, {0, 5, 6, 7, 8, 0}); + actual.set(3, 0, {0, 9, 0, 1, 2, 0}); + actual.set(4, 0, {0, 0, 0, 0, 0, 0}); + + test::Array2D expected{width, height}; + expected.set(0, 0, {1, 1, 2, 3, 4, 4}); + expected.set(1, 0, {1, 1, 2, 3, 4, 4}); + expected.set(2, 0, {5, 5, 6, 7, 8, 8}); + expected.set(3, 0, {9, 9, 0, 1, 2, 2}); + expected.set(4, 0, {9, 9, 0, 1, 2, 2}); + + ImplementsBorder bordered{1, 1, 1, 1}; + test::prepare_borders(INTRINSICCV_BORDER_TYPE_REPLICATE, + &bordered, &actual); + EXPECT_EQ_ARRAY2D(expected, actual); +} + +/// Tests that replicate border strategy works for one channel and two bordering +/// elements in every direction. +TEST(Border, Replicate_1Ch_2Elements) { + using ElementType = uint8_t; + + const size_t width = 6, height = 6; + + test::Array2D actual{width, height}; + actual.set(0, 0, {0, 0, 0, 0, 0, 0}); + actual.set(1, 0, {0, 0, 0, 0, 0, 0}); + actual.set(2, 0, {0, 0, 1, 2, 0, 0}); + actual.set(3, 0, {0, 0, 3, 4, 0, 0}); + actual.set(4, 0, {0, 0, 0, 0, 0, 0}); + actual.set(5, 0, {0, 0, 0, 0, 0, 0}); + + test::Array2D expected{width, height}; + expected.set(0, 0, {1, 1, 1, 2, 2, 2}); + expected.set(1, 0, {1, 1, 1, 2, 2, 2}); + expected.set(2, 0, {1, 1, 1, 2, 2, 2}); + expected.set(3, 0, {3, 3, 3, 4, 4, 4}); + expected.set(4, 0, {3, 3, 3, 4, 4, 4}); + expected.set(5, 0, {3, 3, 3, 4, 4, 4}); + + ImplementsBorder bordered{2, 2, 2, 2}; + test::prepare_borders(INTRINSICCV_BORDER_TYPE_REPLICATE, + &bordered, &actual); + EXPECT_EQ_ARRAY2D(expected, actual); +} + +/// Tests that replicate border strategy works for two channels and one +/// bordering element in every direction. +TEST(Border, Replicate_2Ch_1Element) { + using ElementType = uint8_t; + + const size_t width = 8, height = 5, channels = 2; + + test::Array2D actual{width, height, 0, channels}; + actual.set(0, 0, {0, 0, 0, 0, 0, 0, 0, 0}); + actual.set(1, 0, {0, 0, 1, 2, 3, 4, 0, 0}); + actual.set(2, 0, {0, 0, 5, 6, 7, 8, 0, 0}); + actual.set(3, 0, {0, 0, 9, 1, 2, 3, 0, 0}); + actual.set(4, 0, {0, 0, 0, 0, 0, 0, 0, 0}); + + test::Array2D expected{width, height, 0, channels}; + expected.set(0, 0, {1, 2, 1, 2, 3, 4, 3, 4}); + expected.set(1, 0, {1, 2, 1, 2, 3, 4, 3, 4}); + expected.set(2, 0, {5, 6, 5, 6, 7, 8, 7, 8}); + expected.set(3, 0, {9, 1, 9, 1, 2, 3, 2, 3}); + expected.set(4, 0, {9, 1, 9, 1, 2, 3, 2, 3}); + + ImplementsBorder bordered{1, 1, 1, 1}; + test::prepare_borders(INTRINSICCV_BORDER_TYPE_REPLICATE, + &bordered, &actual); + EXPECT_EQ_ARRAY2D(expected, actual); +} + +/// Tests that replicate border strategy works for two channels and two +/// bordering elements in every direction. +TEST(Border, Replicate_2Ch_2Elements) { + using ElementType = uint8_t; + + const size_t width = 12, height = 6, channels = 2; + + test::Array2D actual{width, height, 0, channels}; + actual.set(0, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + actual.set(1, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + actual.set(2, 0, {0, 0, 0, 0, 1, 2, 3, 4, 0, 0, 0, 0}); + actual.set(3, 0, {0, 0, 0, 0, 5, 6, 7, 8, 0, 0, 0, 0}); + actual.set(4, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + actual.set(5, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + + test::Array2D expected{width, height, 0, channels}; + expected.set(0, 0, {1, 2, 1, 2, 1, 2, 3, 4, 3, 4, 3, 4}); + expected.set(1, 0, {1, 2, 1, 2, 1, 2, 3, 4, 3, 4, 3, 4}); + expected.set(2, 0, {1, 2, 1, 2, 1, 2, 3, 4, 3, 4, 3, 4}); + expected.set(3, 0, {5, 6, 5, 6, 5, 6, 7, 8, 7, 8, 7, 8}); + expected.set(4, 0, {5, 6, 5, 6, 5, 6, 7, 8, 7, 8, 7, 8}); + expected.set(5, 0, {5, 6, 5, 6, 5, 6, 7, 8, 7, 8, 7, 8}); + + ImplementsBorder bordered{2, 2, 2, 2}; + test::prepare_borders(INTRINSICCV_BORDER_TYPE_REPLICATE, + &bordered, &actual); + EXPECT_EQ_ARRAY2D(expected, actual); +} diff --git a/test/framework/test_generator.cpp b/test/framework/test_generator.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a46b036a683427fd77a3539069a8cee97dac66a7 --- /dev/null +++ b/test/framework/test_generator.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include + +#include "framework/generator.h" + +/// Tests test::PseudoRandomNumberGenerator::reset() works. +TEST(PseudoRandomNumberGenerator, Reset) { + using ElementType = uint8_t; + + test::PseudoRandomNumberGenerator generator; + ElementType initial_value = generator.next().value(); + generator.next(); + generator.reset(); + ElementType value = generator.next().value(); + EXPECT_EQ(initial_value, value); +} + +/// Tests test::SequenceGenerator::reset() works. +TEST(SequenceGenerator, Reset) { + using ElementType = uint8_t; + + std::array array{1, 2, 3}; + test::SequenceGenerator generator{array}; + EXPECT_EQ(generator.next().value(), 1); + EXPECT_EQ(generator.next().value(), 2); + EXPECT_EQ(generator.next().value(), 3); + EXPECT_EQ(generator.next(), std::nullopt); + + generator.reset(); + EXPECT_EQ(generator.next().value(), 1); +} diff --git a/test/framework/test_kernel.cpp b/test/framework/test_kernel.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2818859a712aa610ebc64c6cad95a7b34db084d3 --- /dev/null +++ b/test/framework/test_kernel.cpp @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include + +#include "framework/kernel.h" +#include "framework/types.h" + +/// Tests that the constructor of test::Kernel works for odd width and +/// height. +TEST(Kernel, ConstructOdd) { + using ElementType = uint8_t; + + test::Array2D mask{3, 3}; + mask.set(0, 0, {1, 2, 3}); + mask.set(2, 0, {4, 5, 6}); + + test::Kernel kernel{mask}; + EXPECT_EQ(kernel.width(), 3); + EXPECT_EQ(kernel.height(), 3); + EXPECT_EQ(kernel.channels(), 1); + EXPECT_EQ(kernel.anchor().x, 1); + EXPECT_EQ(kernel.anchor().y, 1); + + EXPECT_EQ(kernel.top(), 1); + EXPECT_EQ(kernel.left(), 1); + EXPECT_EQ(kernel.bottom(), 1); + EXPECT_EQ(kernel.right(), 1); + + EXPECT_EQ(kernel.at(0, 0)[0], 1); + EXPECT_EQ(kernel.at(0, 1)[0], 2); + EXPECT_EQ(kernel.at(0, 2)[0], 3); + EXPECT_EQ(kernel.at(1, 0)[0], 0); + EXPECT_EQ(kernel.at(1, 1)[0], 0); + EXPECT_EQ(kernel.at(1, 2)[0], 0); + EXPECT_EQ(kernel.at(2, 0)[0], 4); + EXPECT_EQ(kernel.at(2, 1)[0], 5); + EXPECT_EQ(kernel.at(2, 2)[0], 6); +} + +/// Tests that the constructor of test::Kernel works for even width and +/// height. +TEST(Kernel, ConstructEven) { + using ElementType = uint8_t; + + test::Array2D mask{4, 4}; + test::Kernel kernel{mask}; + EXPECT_EQ(kernel.width(), 4); + EXPECT_EQ(kernel.height(), 4); + EXPECT_EQ(kernel.channels(), 1); + EXPECT_EQ(kernel.anchor().x, 2); + EXPECT_EQ(kernel.anchor().y, 2); + + EXPECT_EQ(kernel.top(), 2); + EXPECT_EQ(kernel.left(), 2); + EXPECT_EQ(kernel.bottom(), 1); + EXPECT_EQ(kernel.right(), 1); +} + +/// Tests that the default constructor of test::Kernel works with all +/// zero arguments. +TEST(Kernel, ConstructEmpty) { + using ElementType = uint8_t; + + test::Array2D mask{0, 0}; + test::Kernel kernel{mask}; + EXPECT_EQ(kernel.width(), 0); + EXPECT_EQ(kernel.height(), 0); + EXPECT_EQ(kernel.channels(), 1); + EXPECT_EQ(kernel.anchor().x, 0); + EXPECT_EQ(kernel.anchor().y, 0); + EXPECT_EQ(kernel.top(), 0); + EXPECT_EQ(kernel.left(), 0); + EXPECT_EQ(kernel.bottom(), 0); + EXPECT_EQ(kernel.right(), 0); +} + +class KernelTestParams { + public: + using InputType = uint8_t; + using IntermediateType = int32_t; + using OutputType = int16_t; +}; // end of class KernelTestParams + +template +class ExampleKernelTest : public test::KernelTest { + using InputType = typename KernelTestParams::InputType; + using IntermediateType = typename KernelTestParams::IntermediateType; + using OutputType = typename KernelTestParams::OutputType; + + void call_api(const test::Array2D *input, + test::Array2D *output, + intrinsiccv_border_type_t border_type) override { + ++api_calls_; + + // Check the expected border type. + EXPECT_EQ(border_type, kBorders[border_count_ % kBorders.size()]); + ++border_count_; + if (border_count_ == kBorders.size()) { + border_count_ = 0; + } + + // Check the expected layout. + const test::ArrayLayout &expected_array_layout = + kArrayLayouts[array_layouts_ % kArrayLayouts.size()]; + + EXPECT_EQ(expected_array_layout.width, input->width()); + EXPECT_EQ(expected_array_layout.height, input->height()); + EXPECT_EQ(expected_array_layout.padding, + input->stride() - input->width() * sizeof(InputType)); + EXPECT_EQ(expected_array_layout.channels, input->channels()); + + EXPECT_EQ(input->width(), output->width()); + EXPECT_EQ(input->height(), output->height()); + // NOTE: This is not expected. + // EXPECT_EQ(input->stride(), output->stride()); + EXPECT_EQ(input->channels(), output->channels()); + + if (border_count_ == 0) { + ++array_layouts_; + } + + // Fake some result. + output->fill(OutputType{0}); + } + + public: + static const std::array, 2> kKernels; + + static constexpr std::array kArrayLayouts = {{ + {3, 2, 10, 1}, + {6, 5, 11, 2}, + {9, 7, 11, 3}, + }}; + + static constexpr std::array kBorders = { + // NOTE: At the time of writing this test only replicate was implemented. + INTRINSICCV_BORDER_TYPE_REPLICATE, + INTRINSICCV_BORDER_TYPE_REPLICATE, + INTRINSICCV_BORDER_TYPE_REPLICATE, + INTRINSICCV_BORDER_TYPE_REPLICATE, + }; + + size_t api_calls_{0}; + size_t array_layouts_{0}; + size_t border_count_{0}; +}; // end of class class ExampleKernelTest + +template +const std::array, 2> + ExampleKernelTest::kKernels = { + test::Kernel{ + test::Array2D{3, 3}}, + test::Kernel{ + test::Array2D{4, 4}}, +}; + +/// Tests that KernelTest::test() works. +TEST(KernelTest, Test) { + test::SequenceGenerator tested_kernels{ + ExampleKernelTest::kKernels}; + test::SequenceGenerator tested_borders{ + ExampleKernelTest::kBorders}; + test::SequenceGenerator tested_array_layouts{ + ExampleKernelTest::kArrayLayouts}; + test::PseudoRandomNumberGenerator + element_generator; + + ExampleKernelTest kernel_test; + kernel_test.test(&tested_kernels, &tested_array_layouts, &tested_borders, + &element_generator); + + EXPECT_EQ(kernel_test.api_calls_, + ExampleKernelTest::kKernels.size() * + ExampleKernelTest::kArrayLayouts.size() * + ExampleKernelTest::kBorders.size()); + EXPECT_EQ(kernel_test.array_layouts_, + ExampleKernelTest::kKernels.size() * + ExampleKernelTest::kArrayLayouts.size()); +} diff --git a/test/framework/test_main.cpp b/test/framework/test_main.cpp index e30a8268b728302165e484311797e69b2f4eebf2..2c88f42a2796598608e62c5312b39d5dd539041e 100644 --- a/test/framework/test_main.cpp +++ b/test/framework/test_main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include "framework/utils.h" @@ -13,16 +14,20 @@ namespace test { size_t Options::vector_length_{16}; +uint64_t Options::seed_{0}; /// Parses command line arguments. static void parse_arguments(int argc, char **argv) { // clang-format off static struct option long_options[] = { {"vector-length", required_argument, nullptr, 'v'}, + {"seed", required_argument, nullptr, 's'}, {0, 0, nullptr, 0} }; // clang-format on + bool is_seed_set = false; + for (;;) { int option_index = 0; int opt = getopt_long(argc, argv, "", long_options, &option_index); @@ -38,11 +43,23 @@ static void parse_arguments(int argc, char **argv) { case 'v': Options::set_vector_length(std::stoi(optarg)); break; + + case 's': + Options::set_seed(std::stoull(optarg)); + is_seed_set = true; + break; } } + // Set a random seed if it is not explicitly specified. + if (!is_seed_set) { + std::random_device rd; + Options::set_seed(static_cast(rd())); + } + std::cout << "Vector length is set to " << Options::vector_length() << " bytes." << std::endl; + std::cout << "Seed is set to " << Options::seed() << "." << std::endl; } } // namespace test diff --git a/test/framework/types.h b/test/framework/types.h new file mode 100644 index 0000000000000000000000000000000000000000..10813a7aa9b464bb04e94fcedb32015d9491b216 --- /dev/null +++ b/test/framework/types.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef INTRINSICCV_TEST_FRAMEWORK_TYPES_H_ +#define INTRINSICCV_TEST_FRAMEWORK_TYPES_H_ + +#include + +namespace test { + +/// Represents a point with two non-negative coordinates where 'x' is the +/// horizontal and 'y' is the vertical component. +struct Point { + size_t x; + size_t y; +}; // end of struct Point + +/// Describes the layout of a two-dimensional array. +struct ArrayLayout { + size_t width; + size_t height; + size_t padding; + size_t channels; +}; // end of struct ArrayLayout + +} // namespace test + +#endif // INTRINSICCV_TEST_FRAMEWORK_TYPES_H_ diff --git a/test/framework/utils.cpp b/test/framework/utils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0640bb2e008451f24e81dd59613e5ee7c7059dd9 --- /dev/null +++ b/test/framework/utils.cpp @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2023 Arm Limited and/or its affiliates +// +// SPDX-License-Identifier: Apache-2.0 + +#include "framework/utils.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace test { + +template +void dump(const TwoDimensional *elements) { + if (!elements) { + return; + } + + auto mask = std::numeric_limits>::max(); + + for (size_t row = 0; row < elements->height(); ++row) { + for (size_t column = 0; column < elements->width(); ++column) { + ElementType value = elements->at(row, column)[0]; + std::cout << std::setw(2 * sizeof(ElementType)) << std::setfill('0') + << std::hex << (static_cast(value) & mask) << " "; + } + + std::cout << std::endl; + } + + if ((elements->height() > 0) && (elements->width() > 0)) { + std::cout << std::endl << std::flush; + } +} + +template void dump(const TwoDimensional *); +template void dump(const TwoDimensional *); +template void dump(const TwoDimensional *); +template void dump(const TwoDimensional *); +template void dump(const TwoDimensional *); +template void dump(const TwoDimensional *); +template void dump(const TwoDimensional *); +template void dump(const TwoDimensional *); + +std::array default_array_layouts(size_t min_width, + size_t min_height) { + size_t vl = test::Options::vector_length(); + size_t width = std::max(min_width, vl); + + return {{ + // clang-format off + // width, height, padding, channels + { min_width, min_height, 0, 1}, + { min_width * 2, min_height, 0, 2}, + { min_width * 3, min_height, 0, 3}, + { min_width, min_height, vl, 1}, + { min_width * 2, min_height, vl, 2}, + { min_width * 3, min_height, vl, 3}, + { width - 1, min_height, 0, 1}, + {2 * (width - 1), min_height + 1, 0, 2}, + {3 * (width - 1), min_height + 2, 0, 3}, + { width - 1, min_height, vl, 1}, + {2 * (width - 1), min_height + 1, vl, 2}, + {3 * (width - 1), min_height + 2, vl, 3}, + // clang-format on + }}; +} + +} // namespace test diff --git a/test/framework/utils.h b/test/framework/utils.h index 2fbc47019fa63b1e3889bad6a93c33a3a2a69a8f..c3ee3baca91f2526523b25b6f28536b0ae16fffa 100644 --- a/test/framework/utils.h +++ b/test/framework/utils.h @@ -10,6 +10,9 @@ #include #include +#include "framework/abstract.h" +#include "framework/types.h" + #define INTRINSICCV_API(name, impl, type) \ template , bool> = true> \ @@ -17,6 +20,21 @@ return &impl; \ } +// Generates a fatal failure with a generic message, and returns with a given +// value. +#define TEST_FAIL_WITH(return_value, message) \ + do { \ + GTEST_MESSAGE_("Failed", ::testing::TestPartResult::kFatalFailure) \ + << message; \ + return (return_value); \ + } while (0 != 0) + +// Returns if the test has any failures. +#define ASSERT_NO_FAILURES() \ + if (::testing::Test::HasFailure()) { \ + return; \ + } + namespace test { class Options { @@ -24,6 +42,9 @@ class Options { /// Returns the vector length being tested. This is in bytes. static size_t vector_length() { return vector_length_; } + /// Returns seed to use. + static uint64_t seed() { return seed_; } + /// Returns the number of lanes in a vector for a given integral type. template , bool> = true> @@ -43,11 +64,24 @@ class Options { vector_length_ = value; } + /// Sets the seed. + static void set_seed(uint64_t value) { seed_ = value; } + private: /// Vector length being tested. static size_t vector_length_; + /// Seed to use. + static uint64_t seed_; }; // end of class Options +/// Prints all the elements in a two-dimensional space. +template +void dump(const TwoDimensional *elements); + +/// Returns an array of default tested layouts. +std::array default_array_layouts(size_t min_width, + size_t min_height); + } // namespace test #endif // INTRINSICCV_TEST_FRAMEWORK_UTILS_H_