diff options
Diffstat (limited to 'test')
121 files changed, 8245 insertions, 1440 deletions
diff --git a/test/bayer-format.cpp b/test/bayer-format.cpp new file mode 100644 index 00000000..f8d19804 --- /dev/null +++ b/test/bayer-format.cpp @@ -0,0 +1,211 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Sebastian Fricke + * + * BayerFormat class tests + */ + +#include <iostream> + +#include <libcamera/transform.h> + +#include "libcamera/internal/bayer_format.h" + +#include "test.h" + +using namespace std; +using namespace libcamera; + +class BayerFormatTest : public Test +{ +protected: + int run() + { + /* An empty Bayer format has to be invalid. */ + BayerFormat bayerFmt; + if (bayerFmt.isValid()) { + cerr << "An empty BayerFormat has to be invalid." + << endl; + return TestFail; + } + + /* A correct Bayer format has to be valid. */ + bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None); + if (!bayerFmt.isValid()) { + cerr << "A correct BayerFormat has to be valid." + << endl; + return TestFail; + } + + /* + * Two bayer formats created with the same order and bit depth + * have to be equal. + */ + bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None); + BayerFormat bayerFmtExpect = BayerFormat(BayerFormat::BGGR, 8, + BayerFormat::Packing::None); + if (bayerFmt != bayerFmtExpect) { + cerr << "Comparison of identical formats failed." + << endl; + return TestFail; + } + + /* + * Two Bayer formats created with the same order but with a + * different bitDepth are not equal. + */ + bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None); + bayerFmtExpect = BayerFormat(BayerFormat::BGGR, 12, + BayerFormat::Packing::None); + if (bayerFmt == bayerFmtExpect) { + cerr << "Comparison of different formats failed." + << endl; + return TestFail; + } + + /* + * Create a Bayer format with a V4L2PixelFormat and check if we + * get the same format after converting back to the V4L2 Format. + */ + V4L2PixelFormat v4l2FmtExpect = V4L2PixelFormat( + V4L2_PIX_FMT_SBGGR8); + bayerFmt = BayerFormat::fromV4L2PixelFormat(v4l2FmtExpect); + V4L2PixelFormat v4l2Fmt = bayerFmt.toV4L2PixelFormat(); + if (v4l2Fmt != v4l2FmtExpect) { + cerr << "Expected: '" << v4l2FmtExpect + << "' got: '" << v4l2Fmt << "'" << endl; + return TestFail; + } + + /* + * Use an empty Bayer format and verify that no matching + * V4L2PixelFormat is found. + */ + v4l2FmtExpect = V4L2PixelFormat(); + bayerFmt = BayerFormat(); + v4l2Fmt = bayerFmt.toV4L2PixelFormat(); + if (v4l2Fmt != v4l2FmtExpect) { + cerr << "Expected: empty V4L2PixelFormat got: '" + << v4l2Fmt << "'" << endl; + return TestFail; + } + + /* + * Check if we get the expected Bayer format BGGR8 + * when we convert the V4L2PixelFormat (V4L2_PIX_FMT_SBGGR8) + * to a Bayer format. + */ + bayerFmtExpect = BayerFormat(BayerFormat::BGGR, 8, + BayerFormat::Packing::None); + v4l2Fmt = V4L2PixelFormat(V4L2_PIX_FMT_SBGGR8); + bayerFmt = BayerFormat::fromV4L2PixelFormat(v4l2Fmt); + if (bayerFmt != bayerFmtExpect) { + cerr << "Expected BayerFormat '" + << bayerFmtExpect << "', got: '" + << bayerFmt << "'" << endl; + return TestFail; + } + + /* + * Confirm that a V4L2PixelFormat that is not found in + * the conversion table, doesn't yield a Bayer format. + */ + V4L2PixelFormat v4l2FmtUnknown = V4L2PixelFormat( + V4L2_PIX_FMT_BGRA444); + bayerFmt = BayerFormat::fromV4L2PixelFormat(v4l2FmtUnknown); + if (bayerFmt.isValid()) { + cerr << "Expected empty BayerFormat got: '" + << bayerFmt << "'" << endl; + return TestFail; + } + + /* + * Test if a valid Bayer format can be converted to a + * string representation. + */ + bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None); + if (bayerFmt.toString() != "BGGR-8") { + cerr << "String representation != 'BGGR-8' (got: '" + << bayerFmt.toString() << "' ) " << endl; + return TestFail; + } + + /* + * Determine if an empty Bayer format results in no + * string representation. + */ + bayerFmt = BayerFormat(); + if (bayerFmt.toString() != "INVALID") { + cerr << "String representation != 'INVALID' (got: '" + << bayerFmt.toString() << "' ) " << endl; + return TestFail; + } + + /* + * Perform a horizontal Flip and make sure that the + * order is adjusted accordingly. + */ + bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None); + bayerFmtExpect = BayerFormat(BayerFormat::GBRG, 8, + BayerFormat::Packing::None); + BayerFormat hFlipFmt = bayerFmt.transform(Transform::HFlip); + if (hFlipFmt != bayerFmtExpect) { + cerr << "Horizontal flip of 'BGGR-8' should result in '" + << bayerFmtExpect << "', got: '" + << hFlipFmt << "'" << endl; + return TestFail; + } + + /* + * Perform a vertical Flip and make sure that + * the order is adjusted accordingly. + */ + bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None); + bayerFmtExpect = BayerFormat(BayerFormat::GRBG, 8, + BayerFormat::Packing::None); + BayerFormat vFlipFmt = bayerFmt.transform(Transform::VFlip); + if (vFlipFmt != bayerFmtExpect) { + cerr << "Vertical flip of 'BGGR-8' should result in '" + << bayerFmtExpect << "', got: '" + << vFlipFmt << "'" << endl; + return TestFail; + } + + /* + * Perform a transposition on a pixel order with both green + * pixels on the bottom left to top right diagonal and make + * sure, that it doesn't change. + */ + bayerFmt = BayerFormat(BayerFormat::BGGR, 8, BayerFormat::Packing::None); + BayerFormat transposeFmt = bayerFmt.transform( + Transform::Transpose); + if (transposeFmt != bayerFmt) { + cerr << "Transpose with both green pixels on the " + << "antidiagonal should not change the order " + << "(got '" << transposeFmt << "')" + << endl; + return TestFail; + } + + /* + * Perform a transposition on an pixel order with red and blue + * on the bottom left to top right diagonal and make sure + * that their positions are switched. + */ + bayerFmt = BayerFormat(BayerFormat::GBRG, 8, BayerFormat::Packing::None); + bayerFmtExpect = BayerFormat(BayerFormat::GRBG, 8, + BayerFormat::Packing::None); + transposeFmt = bayerFmt.transform(Transform::Transpose); + if (transposeFmt != bayerFmtExpect) { + cerr << "Transpose with the red & blue pixels on the " + << "antidiagonal should switch their position " + << "(got '" << transposeFmt << "')" + << endl; + return TestFail; + } + + return TestPass; + } +}; + +TEST_REGISTER(BayerFormatTest) diff --git a/test/byte-stream-buffer.cpp b/test/byte-stream-buffer.cpp index bc1d462e..04aab3d2 100644 --- a/test/byte-stream-buffer.cpp +++ b/test/byte-stream-buffer.cpp @@ -2,13 +2,14 @@ /* * Copyright (C) 2018, Google Inc. * - * byte_stream_buffer.cpp - ByteStreamBuffer tests + * ByteStreamBuffer tests */ #include <array> #include <iostream> -#include "byte_stream_buffer.h" +#include "libcamera/internal/byte_stream_buffer.h" + #include "test.h" using namespace std; @@ -19,7 +20,12 @@ class ByteStreamBufferTest : public Test protected: int run() { - std::array<uint8_t, 100> data; + /* + * gcc 11.1.0 incorrectly raises a maybe-uninitialized warning + * when calling data.size() below (if the address sanitizer is + * disabled). Silence it by initializing the array. + */ + std::array<uint8_t, 100> data = {}; unsigned int i; uint32_t value; int ret; diff --git a/test/camera-sensor.cpp b/test/camera-sensor.cpp index 27c190fe..869c7889 100644 --- a/test/camera-sensor.cpp +++ b/test/camera-sensor.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * camera-sensor.cpp - Camera sensor tests + * Camera sensor tests */ #include <algorithm> @@ -10,11 +10,13 @@ #include <linux/media-bus-format.h> -#include "camera_sensor.h" -#include "device_enumerator.h" -#include "media_device.h" -#include "utils.h" -#include "v4l2_subdevice.h" +#include <libcamera/base/utils.h> + +#include "libcamera/internal/camera_lens.h" +#include "libcamera/internal/camera_sensor.h" +#include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/media_device.h" +#include "libcamera/internal/v4l2_subdevice.h" #include "test.h" @@ -50,17 +52,27 @@ protected: return TestFail; } - sensor_ = new CameraSensor(entity); - if (sensor_->init() < 0) { + sensor_ = CameraSensorFactoryBase::create(entity); + if (!sensor_) { cerr << "Unable to initialise camera sensor" << endl; return TestFail; } + lens_ = sensor_->focusLens(); + if (lens_) + cout << "Found lens controller" << endl; + return TestPass; } int run() { + if (sensor_->model() != "Sensor A") { + cerr << "Incorrect sensor model '" << sensor_->model() + << "'" << endl; + return TestFail; + } + const std::vector<unsigned int> &codes = sensor_->mbusCodes(); auto iter = std::find(codes.begin(), codes.end(), MEDIA_BUS_FMT_ARGB8888_1X32); @@ -69,7 +81,7 @@ protected: return TestFail; } - const std::vector<Size> &sizes = sensor_->sizes(); + const std::vector<Size> &sizes = sensor_->sizes(*iter); auto iter2 = std::find(sizes.begin(), sizes.end(), Size(4096, 2160)); if (iter2 == sizes.end()) { @@ -79,8 +91,7 @@ protected: const Size &resolution = sensor_->resolution(); if (resolution != Size(4096, 2160)) { - cerr << "Incorrect sensor resolution " - << resolution.toString() << endl; + cerr << "Incorrect sensor resolution " << resolution << endl; return TestFail; } @@ -89,11 +100,16 @@ protected: MEDIA_BUS_FMT_SBGGR10_1X10, MEDIA_BUS_FMT_BGR888_1X24 }, Size(1024, 768)); - if (format.mbus_code != MEDIA_BUS_FMT_SBGGR10_1X10 || + if (format.code != MEDIA_BUS_FMT_SBGGR10_1X10 || format.size != Size(4096, 2160)) { cerr << "Failed to get a suitable format, expected 4096x2160-0x" << utils::hex(MEDIA_BUS_FMT_SBGGR10_1X10) - << ", got " << format.toString() << endl; + << ", got " << format << endl; + return TestFail; + } + + if (lens_ && lens_->setFocusPosition(10)) { + cerr << "Failed to set lens focus position" << endl; return TestFail; } @@ -102,13 +118,13 @@ protected: void cleanup() { - delete sensor_; } private: std::unique_ptr<DeviceEnumerator> enumerator_; std::shared_ptr<MediaDevice> media_; - CameraSensor *sensor_; + std::unique_ptr<CameraSensor> sensor_; + CameraLens *lens_; }; TEST_REGISTER(CameraSensorTest) diff --git a/test/camera/buffer_import.cpp b/test/camera/buffer_import.cpp index 3f392cdc..815d1cae 100644 --- a/test/camera/buffer_import.cpp +++ b/test/camera/buffer_import.cpp @@ -12,15 +12,20 @@ #include <numeric> #include <vector> -#include "device_enumerator.h" -#include "media_device.h" -#include "v4l2_videodevice.h" +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> + +#include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/media_device.h" +#include "libcamera/internal/v4l2_videodevice.h" #include "buffer_source.h" #include "camera_test.h" #include "test.h" using namespace libcamera; +using namespace std::chrono_literals; namespace { @@ -28,12 +33,13 @@ class BufferImportTest : public CameraTest, public Test { public: BufferImportTest() - : CameraTest("VIMC Sensor B") + : CameraTest("platform/vimc.0 Sensor B") { } protected: - void bufferComplete(Request *request, FrameBuffer *buffer) + void bufferComplete([[maybe_unused]] Request *request, + FrameBuffer *buffer) { if (buffer->metadata().status != FrameMetadata::FrameSuccess) return; @@ -46,17 +52,19 @@ protected: if (request->status() != Request::RequestComplete) return; - const std::map<Stream *, FrameBuffer *> &buffers = request->buffers(); + const Request::BufferMap &buffers = request->buffers(); completeRequestsCount_++; /* Create a new request. */ - Stream *stream = buffers.begin()->first; + const Stream *stream = buffers.begin()->first; FrameBuffer *buffer = buffers.begin()->second; - request = camera_->createRequest(); + request->reuse(); request->addBuffer(stream, buffer); camera_->queueRequest(request); + + dispatcher_->interrupt(); } int init() override @@ -70,6 +78,8 @@ protected: return TestFail; } + dispatcher_ = Thread::current()->eventDispatcher(); + return TestPass; } @@ -94,9 +104,8 @@ protected: if (ret != TestPass) return ret; - std::vector<Request *> requests; for (const std::unique_ptr<FrameBuffer> &buffer : source.buffers()) { - Request *request = camera_->createRequest(); + std::unique_ptr<Request> request = camera_->createRequest(); if (!request) { std::cout << "Failed to create request" << std::endl; return TestFail; @@ -107,7 +116,7 @@ protected: return TestFail; } - requests.push_back(request); + requests_.push_back(std::move(request)); } completeRequestsCount_ = 0; @@ -121,24 +130,27 @@ protected: return TestFail; } - for (Request *request : requests) { - if (camera_->queueRequest(request)) { + for (std::unique_ptr<Request> &request : requests_) { + if (camera_->queueRequest(request.get())) { std::cout << "Failed to queue request" << std::endl; return TestFail; } } - EventDispatcher *dispatcher = cm_->eventDispatcher(); + const unsigned int nFrames = cfg.bufferCount * 2; Timer timer; - timer.start(1000); - while (timer.isRunning()) - dispatcher->processEvents(); + timer.start(500ms * nFrames); + while (timer.isRunning()) { + dispatcher_->processEvents(); + if (completeRequestsCount_ > nFrames) + break; + } - if (completeRequestsCount_ <= cfg.bufferCount * 2) { + if (completeRequestsCount_ < nFrames) { std::cout << "Failed to capture enough frames (got " << completeRequestsCount_ << " expected at least " - << cfg.bufferCount * 2 << ")" << std::endl; + << nFrames << ")" << std::endl; return TestFail; } @@ -156,6 +168,10 @@ protected: } private: + EventDispatcher *dispatcher_; + + std::vector<std::unique_ptr<Request>> requests_; + unsigned int completeBuffersCount_; unsigned int completeRequestsCount_; std::unique_ptr<CameraConfiguration> config_; @@ -163,4 +179,4 @@ private: } /* namespace */ -TEST_REGISTER(BufferImportTest); +TEST_REGISTER(BufferImportTest) diff --git a/test/camera/camera_reconfigure.cpp b/test/camera/camera_reconfigure.cpp new file mode 100644 index 00000000..06c87730 --- /dev/null +++ b/test/camera/camera_reconfigure.cpp @@ -0,0 +1,264 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Google Inc. + * + * Test: + * - Multiple reconfigurations of the Camera without stopping the CameraManager + * - Validate there are no file descriptor leaks when using IPC + */ + +#include <dirent.h> +#include <fstream> +#include <iostream> + +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/file.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> + +#include <libcamera/framebuffer_allocator.h> + +#include "camera_test.h" +#include "test.h" + +using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; + +namespace { + +class CameraReconfigure : public CameraTest, public Test +{ +public: + /* Initialize CameraTest with isolated IPA */ + CameraReconfigure() + : CameraTest(kCamId_, true) + { + } + +private: + static constexpr const char *kCamId_ = "platform/vimc.0 Sensor B"; + static constexpr const char *kIpaProxyName_ = "vimc_ipa_proxy"; + static constexpr unsigned int kNumOfReconfigures_ = 10; + + void requestComplete(Request *request) + { + if (request->status() != Request::RequestComplete) + return; + + const Request::BufferMap &buffers = request->buffers(); + + const Stream *stream = buffers.begin()->first; + FrameBuffer *buffer = buffers.begin()->second; + + /* Reuse the request and re-queue it with the same buffers. */ + request->reuse(); + request->addBuffer(stream, buffer); + camera_->queueRequest(request); + } + + int startAndStop() + { + StreamConfiguration &cfg = config_->at(0); + + if (camera_->acquire()) { + cerr << "Failed to acquire the camera" << endl; + return TestFail; + } + + if (camera_->configure(config_.get())) { + cerr << "Failed to set default configuration" << endl; + return TestFail; + } + + Stream *stream = cfg.stream(); + + /* + * The configuration is consistent so we can re-use the + * same buffer allocation for each run. + */ + if (!allocated_) { + int ret = allocator_->allocate(stream); + if (ret < 0) { + cerr << "Failed to allocate buffers" << endl; + return TestFail; + } + allocated_ = true; + } + + for (const unique_ptr<FrameBuffer> &buffer : allocator_->buffers(stream)) { + unique_ptr<Request> request = camera_->createRequest(); + if (!request) { + cerr << "Failed to create request" << endl; + return TestFail; + } + + if (request->addBuffer(stream, buffer.get())) { + cerr << "Failed to associate buffer with request" << endl; + return TestFail; + } + + requests_.push_back(std::move(request)); + } + + camera_->requestCompleted.connect(this, &CameraReconfigure::requestComplete); + + if (camera_->start()) { + cerr << "Failed to start camera" << endl; + return TestFail; + } + + for (unique_ptr<Request> &request : requests_) { + if (camera_->queueRequest(request.get())) { + cerr << "Failed to queue request" << endl; + return TestFail; + } + } + + EventDispatcher *dispatcher = Thread::current()->eventDispatcher(); + + Timer timer; + timer.start(100ms); + while (timer.isRunning()) + dispatcher->processEvents(); + + if (camera_->stop()) { + cerr << "Failed to stop camera" << endl; + return TestFail; + } + + if (camera_->release()) { + cerr << "Failed to release camera" << endl; + return TestFail; + } + + camera_->requestCompleted.disconnect(this); + + requests_.clear(); + + return 0; + } + + int fdsOpen(pid_t pid) + { + string proxyFdPath = "/proc/" + to_string(pid) + "/fd"; + DIR *dir; + struct dirent *ptr; + unsigned int openFds = 0; + + dir = opendir(proxyFdPath.c_str()); + if (dir == nullptr) { + int err = errno; + cerr << "Error opening " << proxyFdPath << ": " + << strerror(-err) << endl; + return 0; + } + + while ((ptr = readdir(dir)) != nullptr) { + if ((strcmp(ptr->d_name, ".") == 0) || + (strcmp(ptr->d_name, "..") == 0)) + continue; + + openFds++; + } + closedir(dir); + + return openFds; + } + + pid_t findProxyPid() + { + string proxyPid; + string proxyName(kIpaProxyName_); + DIR *dir; + struct dirent *ptr; + + dir = opendir("/proc"); + while ((ptr = readdir(dir)) != nullptr) { + if (ptr->d_type != DT_DIR) + continue; + + string pname("/proc/" + string(ptr->d_name) + "/comm"); + if (File::exists(pname)) { + ifstream pfile(pname.c_str()); + string comm; + getline(pfile, comm); + pfile.close(); + + proxyPid = comm == proxyName ? string(ptr->d_name) : ""; + } + + if (!proxyPid.empty()) + break; + } + closedir(dir); + + if (!proxyPid.empty()) + return atoi(proxyPid.c_str()); + + return -1; + } + + int init() override + { + if (status_ != TestPass) + return status_; + + config_ = camera_->generateConfiguration({ StreamRole::StillCapture }); + if (!config_ || config_->size() != 1) { + cerr << "Failed to generate default configuration" << endl; + return TestFail; + } + + allocator_ = make_unique<FrameBufferAllocator>(camera_); + allocated_ = false; + + return TestPass; + } + + int run() override + { + unsigned int openFdsAtStart = 0; + unsigned int openFds = 0; + + pid_t proxyPid = findProxyPid(); + if (proxyPid < 0) { + cerr << "Cannot find " << kIpaProxyName_ + << " pid, exiting" << endl; + return TestFail; + } + + openFdsAtStart = fdsOpen(proxyPid); + for (unsigned int i = 0; i < kNumOfReconfigures_; i++) { + startAndStop(); + openFds = fdsOpen(proxyPid); + if (openFds == 0) { + cerr << "No open fds found whereas " + << "open fds at start: " << openFdsAtStart + << endl; + return TestFail; + } + + if (openFds != openFdsAtStart) { + cerr << "Leaking fds for " << kIpaProxyName_ + << " - Open fds: " << openFds << " vs " + << "Open fds at start: " << openFdsAtStart + << endl; + return TestFail; + } + } + + return TestPass; + } + + bool allocated_; + + vector<unique_ptr<Request>> requests_; + + unique_ptr<CameraConfiguration> config_; + unique_ptr<FrameBufferAllocator> allocator_; +}; + +} /* namespace */ + +TEST_REGISTER(CameraReconfigure) diff --git a/test/camera/capture.cpp b/test/camera/capture.cpp index f6b2f348..8766fb19 100644 --- a/test/camera/capture.cpp +++ b/test/camera/capture.cpp @@ -7,10 +7,18 @@ #include <iostream> +#include <libcamera/framebuffer_allocator.h> + +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> + #include "camera_test.h" #include "test.h" +using namespace libcamera; using namespace std; +using namespace std::chrono_literals; namespace { @@ -18,7 +26,7 @@ class Capture : public CameraTest, public Test { public: Capture() - : CameraTest("VIMC Sensor B") + : CameraTest("platform/vimc.0 Sensor B") { } @@ -26,7 +34,8 @@ protected: unsigned int completeBuffersCount_; unsigned int completeRequestsCount_; - void bufferComplete(Request *request, FrameBuffer *buffer) + void bufferComplete([[maybe_unused]] Request *request, + FrameBuffer *buffer) { if (buffer->metadata().status != FrameMetadata::FrameSuccess) return; @@ -39,17 +48,19 @@ protected: if (request->status() != Request::RequestComplete) return; - const std::map<Stream *, FrameBuffer *> &buffers = request->buffers(); + const Request::BufferMap &buffers = request->buffers(); completeRequestsCount_++; /* Create a new request. */ - Stream *stream = buffers.begin()->first; + const Stream *stream = buffers.begin()->first; FrameBuffer *buffer = buffers.begin()->second; - request = camera_->createRequest(); + request->reuse(); request->addBuffer(stream, buffer); camera_->queueRequest(request); + + dispatcher_->interrupt(); } int init() override @@ -64,6 +75,7 @@ protected: } allocator_ = new FrameBufferAllocator(camera_); + dispatcher_ = Thread::current()->eventDispatcher(); return TestPass; } @@ -93,20 +105,19 @@ protected: if (ret < 0) return TestFail; - std::vector<Request *> requests; for (const std::unique_ptr<FrameBuffer> &buffer : allocator_->buffers(stream)) { - Request *request = camera_->createRequest(); + std::unique_ptr<Request> request = camera_->createRequest(); if (!request) { cout << "Failed to create request" << endl; return TestFail; } if (request->addBuffer(stream, buffer.get())) { - cout << "Failed to associating buffer with request" << endl; + cout << "Failed to associate buffer with request" << endl; return TestFail; } - requests.push_back(request); + requests_.push_back(std::move(request)); } completeRequestsCount_ = 0; @@ -120,26 +131,27 @@ protected: return TestFail; } - for (Request *request : requests) { - if (camera_->queueRequest(request)) { + for (std::unique_ptr<Request> &request : requests_) { + if (camera_->queueRequest(request.get())) { cout << "Failed to queue request" << endl; return TestFail; } } - EventDispatcher *dispatcher = cm_->eventDispatcher(); + unsigned int nFrames = allocator_->buffers(stream).size() * 2; Timer timer; - timer.start(1000); - while (timer.isRunning()) - dispatcher->processEvents(); - - unsigned int nbuffers = allocator_->buffers(stream).size(); + timer.start(500ms * nFrames); + while (timer.isRunning()) { + dispatcher_->processEvents(); + if (completeRequestsCount_ > nFrames) + break; + } - if (completeRequestsCount_ <= nbuffers * 2) { + if (completeRequestsCount_ < nFrames) { cout << "Failed to capture enough frames (got " << completeRequestsCount_ << " expected at least " - << nbuffers * 2 << ")" << endl; + << nFrames * 2 << ")" << endl; return TestFail; } @@ -156,10 +168,14 @@ protected: return TestPass; } + EventDispatcher *dispatcher_; + + std::vector<std::unique_ptr<Request>> requests_; + std::unique_ptr<CameraConfiguration> config_; FrameBufferAllocator *allocator_; }; } /* namespace */ -TEST_REGISTER(Capture); +TEST_REGISTER(Capture) diff --git a/test/camera/configuration_default.cpp b/test/camera/configuration_default.cpp index 31c908d2..209eb6a5 100644 --- a/test/camera/configuration_default.cpp +++ b/test/camera/configuration_default.cpp @@ -10,6 +10,7 @@ #include "camera_test.h" #include "test.h" +using namespace libcamera; using namespace std; namespace { @@ -18,7 +19,7 @@ class ConfigurationDefault : public CameraTest, public Test { public: ConfigurationDefault() - : CameraTest("VIMC Sensor B") + : CameraTest("platform/vimc.0 Sensor B") { } @@ -56,4 +57,4 @@ protected: } /* namespace */ -TEST_REGISTER(ConfigurationDefault); +TEST_REGISTER(ConfigurationDefault) diff --git a/test/camera/configuration_set.cpp b/test/camera/configuration_set.cpp index b4b59681..4281a1c4 100644 --- a/test/camera/configuration_set.cpp +++ b/test/camera/configuration_set.cpp @@ -10,6 +10,7 @@ #include "camera_test.h" #include "test.h" +using namespace libcamera; using namespace std; namespace { @@ -18,7 +19,7 @@ class ConfigurationSet : public CameraTest, public Test { public: ConfigurationSet() - : CameraTest("VIMC Sensor B") + : CameraTest("platform/vimc.0 Sensor B") { } @@ -102,4 +103,4 @@ protected: } /* namespace */ -TEST_REGISTER(ConfigurationSet); +TEST_REGISTER(ConfigurationSet) diff --git a/test/camera/meson.build b/test/camera/meson.build index e2a6660a..4f9f8c8c 100644 --- a/test/camera/meson.build +++ b/test/camera/meson.build @@ -1,17 +1,20 @@ +# SPDX-License-Identifier: CC0-1.0 + # Tests are listed in order of complexity. # They are not alphabetically sorted. camera_tests = [ - [ 'configuration_default', 'configuration_default.cpp' ], - [ 'configuration_set', 'configuration_set.cpp' ], - [ 'buffer_import', 'buffer_import.cpp' ], - [ 'statemachine', 'statemachine.cpp' ], - [ 'capture', 'capture.cpp' ], + {'name': 'configuration_default', 'sources': ['configuration_default.cpp']}, + {'name': 'configuration_set', 'sources': ['configuration_set.cpp']}, + {'name': 'buffer_import', 'sources': ['buffer_import.cpp']}, + {'name': 'statemachine', 'sources': ['statemachine.cpp']}, + {'name': 'capture', 'sources': ['capture.cpp']}, + {'name': 'camera_reconfigure', 'sources': ['camera_reconfigure.cpp']}, ] -foreach t : camera_tests - exe = executable(t[0], t[1], - dependencies : libcamera_dep, +foreach test : camera_tests + exe = executable(test['name'], test['sources'], + dependencies : libcamera_private, link_with : test_libraries, include_directories : test_includes_internal) - test(t[0], exe, suite : 'camera', is_parallel : false) + test(test['name'], exe, suite : 'camera', is_parallel : false) endforeach diff --git a/test/camera/statemachine.cpp b/test/camera/statemachine.cpp index 325b4674..9c2b0c6a 100644 --- a/test/camera/statemachine.cpp +++ b/test/camera/statemachine.cpp @@ -7,9 +7,12 @@ #include <iostream> +#include <libcamera/framebuffer_allocator.h> + #include "camera_test.h" #include "test.h" +using namespace libcamera; using namespace std; namespace { @@ -18,7 +21,7 @@ class Statemachine : public CameraTest, public Test { public: Statemachine() - : CameraTest("VIMC Sensor B") + : CameraTest("platform/vimc.0 Sensor B") { } @@ -39,13 +42,13 @@ protected: if (camera_->queueRequest(&request) != -EACCES) return TestFail; - if (camera_->stop() != -EACCES) - return TestFail; - /* Test operations which should pass. */ if (camera_->release()) return TestFail; + if (camera_->stop()) + return TestFail; + /* Test valid state transitions, end in Acquired state. */ if (camera_->acquire()) return TestFail; @@ -69,7 +72,8 @@ protected: if (camera_->queueRequest(&request) != -EACCES) return TestFail; - if (camera_->stop() != -EACCES) + /* Test operations which should pass. */ + if (camera_->stop()) return TestFail; /* Test valid state transitions, end in Configured state. */ @@ -95,16 +99,13 @@ protected: if (camera_->queueRequest(&request1) != -EACCES) return TestFail; - if (camera_->stop() != -EACCES) - return TestFail; - /* Test operations which should pass. */ - Request *request2 = camera_->createRequest(); + std::unique_ptr<Request> request2 = camera_->createRequest(); if (!request2) return TestFail; - /* Never handed to hardware so need to manually delete it. */ - delete request2; + if (camera_->stop()) + return TestFail; /* Test valid state transitions, end in Running state. */ if (camera_->release()) @@ -144,7 +145,7 @@ protected: return TestFail; /* Test operations which should pass. */ - Request *request = camera_->createRequest(); + std::unique_ptr<Request> request = camera_->createRequest(); if (!request) return TestFail; @@ -152,7 +153,7 @@ protected: if (request->addBuffer(stream, allocator_->buffers(stream)[0].get())) return TestFail; - if (camera_->queueRequest(request)) + if (camera_->queueRequest(request.get())) return TestFail; /* Test valid state transitions, end in Available state. */ @@ -212,4 +213,4 @@ protected: } /* namespace */ -TEST_REGISTER(Statemachine); +TEST_REGISTER(Statemachine) diff --git a/test/color-space.cpp b/test/color-space.cpp new file mode 100644 index 00000000..7d45b217 --- /dev/null +++ b/test/color-space.cpp @@ -0,0 +1,105 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Laurent Pinchart <laurent.pinchart@ideasonboard.com> + * + * libcamera ColorSpace test + */ + +#include <array> +#include <iostream> + +#include <libcamera/color_space.h> + +#include "test.h" + +using namespace libcamera; +using namespace std; + +class ColorSpaceTest : public Test +{ +protected: + int run() + { + if (ColorSpace::toString(std::nullopt) != "Unset") { + std::cerr << "Conversion from nullopt to string failed" << std::endl; + return TestFail; + } + + const std::array<std::pair<ColorSpace, std::string>, 10> colorSpaces = { { + { ColorSpace::Raw, "RAW" }, + { ColorSpace::Srgb, "sRGB" }, + { ColorSpace::Sycc, "sYCC" }, + { ColorSpace::Smpte170m, "SMPTE170M" }, + { ColorSpace::Rec709, "Rec709" }, + { ColorSpace::Rec2020, "Rec2020" }, + { + ColorSpace{ + ColorSpace::Primaries::Raw, + ColorSpace::TransferFunction::Linear, + ColorSpace::YcbcrEncoding::None, + ColorSpace::Range::Limited + }, + "RAW/Linear/None/Limited" + }, { + ColorSpace{ + ColorSpace::Primaries::Smpte170m, + ColorSpace::TransferFunction::Srgb, + ColorSpace::YcbcrEncoding::Rec601, + ColorSpace::Range::Full + }, + "SMPTE170M/sRGB/Rec601/Full" + }, { + ColorSpace{ + ColorSpace::Primaries::Rec709, + ColorSpace::TransferFunction::Rec709, + ColorSpace::YcbcrEncoding::Rec709, + ColorSpace::Range::Full + }, + "Rec709/Rec709/Rec709/Full" + }, { + ColorSpace{ + ColorSpace::Primaries::Rec2020, + ColorSpace::TransferFunction::Linear, + ColorSpace::YcbcrEncoding::Rec2020, + ColorSpace::Range::Limited + }, + "Rec2020/Linear/Rec2020/Limited" + }, + } }; + + for (const auto &[colorSpace, name] : colorSpaces) { + if (colorSpace.toString() != name) { + std::cerr + << "Conversion from ColorSpace to string failed: " + << "expected " << name + << ", got " << colorSpace.toString() + << std::endl; + return TestFail; + } + + if (ColorSpace::fromString(name) != colorSpace) { + std::cerr + << "Conversion from string " + << name << " to ColorSpace failed" + << std::endl; + return TestFail; + } + } + + if (ColorSpace::fromString("Invalid")) { + std::cerr << "Conversion from invalid name string to color space succeeded" + << std::endl; + return TestFail; + } + + if (ColorSpace::fromString("Rec709/Rec709/Rec710/Limited")) { + std::cerr << "Conversion from invalid component string to color space succeeded" + << std::endl; + return TestFail; + } + + return TestPass; + } +}; + +TEST_REGISTER(ColorSpaceTest) diff --git a/test/controls/control_info.cpp b/test/controls/control_info.cpp index 1e05e131..e1bb43f0 100644 --- a/test/controls/control_info.cpp +++ b/test/controls/control_info.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * control_info.cpp - ControlInfo tests + * ControlInfo tests */ #include <iostream> @@ -26,24 +26,59 @@ protected: */ ControlInfo brightness; - if (brightness.min().get<int32_t>() != 0 || - brightness.max().get<int32_t>() != 0) { + if (brightness.min().type() != ControlType::ControlTypeNone || + brightness.max().type() != ControlType::ControlTypeNone || + brightness.def().type() != ControlType::ControlTypeNone) { cout << "Invalid control range for Brightness" << endl; return TestFail; } /* * Test information retrieval from a control with a minimum and - * a maximum value. + * a maximum value, and an implicit default value. */ ControlInfo contrast(10, 200); if (contrast.min().get<int32_t>() != 10 || - contrast.max().get<int32_t>() != 200) { + contrast.max().get<int32_t>() != 200 || + !contrast.def().isNone()) { cout << "Invalid control range for Contrast" << endl; return TestFail; } + /* + * Test information retrieval from a control with boolean + * values. + */ + ControlInfo aeEnable({ false, true }, false); + + if (aeEnable.min().get<bool>() != false || + aeEnable.def().get<bool>() != false || + aeEnable.max().get<bool>() != true) { + cout << "Invalid control range for AeEnable" << endl; + return TestFail; + } + + if (aeEnable.values()[0].get<bool>() != false || + aeEnable.values()[1].get<bool>() != true) { + cout << "Invalid control values for AeEnable" << endl; + return TestFail; + } + + ControlInfo awbEnable(true); + + if (awbEnable.min().get<bool>() != true || + awbEnable.def().get<bool>() != true || + awbEnable.max().get<bool>() != true) { + cout << "Invalid control range for AwbEnable" << endl; + return TestFail; + } + + if (awbEnable.values()[0].get<bool>() != true) { + cout << "Invalid control values for AwbEnable" << endl; + return TestFail; + } + return TestPass; } }; diff --git a/test/controls/control_info_map.cpp b/test/controls/control_info_map.cpp index eeb702db..b0be14b5 100644 --- a/test/controls/control_info_map.cpp +++ b/test/controls/control_info_map.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * control_info.cpp - ControlInfoMap tests + * ControlInfoMap tests */ #include <iostream> @@ -12,7 +12,7 @@ #include <libcamera/control_ids.h> #include <libcamera/controls.h> -#include "camera_controls.h" +#include "libcamera/internal/camera_controls.h" #include "camera_test.h" #include "test.h" @@ -24,7 +24,7 @@ class ControlInfoMapTest : public CameraTest, public Test { public: ControlInfoMapTest() - : CameraTest("VIMC Sensor B") + : CameraTest("platform/vimc.0 Sensor B") { } @@ -75,6 +75,13 @@ protected: return TestFail; } + /* Test looking up a control on a default-constructed infoMap */ + const ControlInfoMap emptyInfoMap; + if (emptyInfoMap.find(12345) != emptyInfoMap.end()) { + cerr << "find() on empty ControlInfoMap failed" << endl; + return TestFail; + } + return TestPass; } }; diff --git a/test/controls/control_list.cpp b/test/controls/control_list.cpp index 5374c6f9..e27325c3 100644 --- a/test/controls/control_list.cpp +++ b/test/controls/control_list.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * control_list.cpp - ControlList tests + * ControlList tests */ #include <iostream> @@ -12,7 +12,7 @@ #include <libcamera/control_ids.h> #include <libcamera/controls.h> -#include "camera_controls.h" +#include "libcamera/internal/camera_controls.h" #include "camera_test.h" #include "test.h" @@ -24,7 +24,7 @@ class ControlListTest : public CameraTest, public Test { public: ControlListTest() - : CameraTest("VIMC Sensor B") + : CameraTest("platform/vimc.0 Sensor B") { } @@ -50,7 +50,7 @@ protected: return TestFail; } - if (list.contains(controls::Brightness)) { + if (list.get(controls::Brightness)) { cout << "List should not contain Brightness control" << endl; return TestFail; } @@ -68,7 +68,7 @@ protected: * Set a control, and verify that the list now contains it, and * nothing else. */ - list.set(controls::Brightness, 255); + list.set(controls::Brightness, -0.5f); if (list.empty()) { cout << "List should not be empty" << endl; @@ -80,7 +80,7 @@ protected: return TestFail; } - if (!list.contains(controls::Brightness)) { + if (!list.get(controls::Brightness)) { cout << "List should contain Brightness control" << endl; return TestFail; } @@ -94,28 +94,29 @@ protected: return TestFail; } - if (list.get(controls::Brightness) != 255) { + if (list.get(controls::Brightness) != -0.5f) { cout << "Incorrest Brightness control value" << endl; return TestFail; } - if (list.contains(controls::Contrast)) { + if (list.get(controls::Contrast)) { cout << "List should not contain Contract control" << endl; return TestFail; } /* Update the first control and set a second one. */ - list.set(controls::Brightness, 64); - list.set(controls::Contrast, 128); + list.set(controls::Brightness, 0.0f); + list.set(controls::Contrast, 1.5f); - if (!list.contains(controls::Contrast) || - !list.contains(controls::Contrast)) { - cout << "List should contain Contrast control" << endl; + if (!list.get(controls::Brightness) || + !list.get(controls::Contrast)) { + cout << "List should contain Brightness and Contrast controls" + << endl; return TestFail; } - if (list.get(controls::Brightness) != 64 || - list.get(controls::Contrast) != 128) { + if (list.get(controls::Brightness) != 0.0f || + list.get(controls::Contrast) != 1.5f) { cout << "Failed to retrieve control value" << endl; return TestFail; } @@ -124,11 +125,11 @@ protected: * Update both controls and verify that the container doesn't * grow. */ - list.set(controls::Brightness, 10); - list.set(controls::Contrast, 20); + list.set(controls::Brightness, 0.5f); + list.set(controls::Contrast, 1.1f); - if (list.get(controls::Brightness) != 10 || - list.get(controls::Contrast) != 20) { + if (list.get(controls::Brightness) != 0.5f || + list.get(controls::Contrast) != 1.1f) { cout << "Failed to update control value" << endl; return TestFail; } @@ -144,11 +145,107 @@ protected: */ list.set(controls::AwbEnable, true); - if (list.contains(controls::AwbEnable)) { + if (list.get(controls::AwbEnable)) { cout << "List shouldn't contain AwbEnable control" << endl; return TestFail; } + /* + * Create a new list with a new control and merge it with the + * existing one, verifying that the existing controls + * values don't get overwritten. + */ + ControlList mergeList(controls::controls, &validator); + mergeList.set(controls::Brightness, 0.7f); + mergeList.set(controls::Saturation, 0.4f); + + mergeList.merge(list); + if (mergeList.size() != 3) { + cout << "Merged list should contain three elements" << endl; + return TestFail; + } + + if (list.size() != 2) { + cout << "The list to merge should contain two elements" + << endl; + return TestFail; + } + + if (!mergeList.get(controls::Brightness) || + !mergeList.get(controls::Contrast) || + !mergeList.get(controls::Saturation)) { + cout << "Merged list does not contain all controls" << endl; + return TestFail; + } + + if (mergeList.get(controls::Brightness) != 0.7f) { + cout << "Brightness control value changed after merging lists" + << endl; + return TestFail; + } + + if (mergeList.get(controls::Contrast) != 1.1f) { + cout << "Contrast control value changed after merging lists" + << endl; + return TestFail; + } + + if (mergeList.get(controls::Saturation) != 0.4f) { + cout << "Saturation control value changed after merging lists" + << endl; + return TestFail; + } + + /* + * Create two lists with overlapping controls. Merge them with + * overwriteExisting = true, verifying that the existing control + * values *get* overwritten. + */ + mergeList.clear(); + mergeList.set(controls::Brightness, 0.7f); + mergeList.set(controls::Saturation, 0.4f); + + list.clear(); + list.set(controls::Brightness, 0.5f); + list.set(controls::Contrast, 1.1f); + + mergeList.merge(list, ControlList::MergePolicy::OverwriteExisting); + if (mergeList.size() != 3) { + cout << "Merged list should contain three elements" << endl; + return TestFail; + } + + if (list.size() != 2) { + cout << "The list to merge should contain two elements" + << endl; + return TestFail; + } + + if (!mergeList.get(controls::Brightness) || + !mergeList.get(controls::Contrast) || + !mergeList.get(controls::Saturation)) { + cout << "Merged list does not contain all controls" << endl; + return TestFail; + } + + if (mergeList.get(controls::Brightness) != 0.5f) { + cout << "Brightness control value did not change after merging lists" + << endl; + return TestFail; + } + + if (mergeList.get(controls::Contrast) != 1.1f) { + cout << "Contrast control value changed after merging lists" + << endl; + return TestFail; + } + + if (mergeList.get(controls::Saturation) != 0.4f) { + cout << "Saturation control value changed after merging lists" + << endl; + return TestFail; + } + return TestPass; } }; diff --git a/test/controls/control_value.cpp b/test/controls/control_value.cpp index ad8e05d0..5084fd0c 100644 --- a/test/controls/control_value.cpp +++ b/test/controls/control_value.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * control_value.cpp - ControlValue tests + * ControlValue tests */ #include <algorithm> @@ -110,6 +110,86 @@ protected: } /* + * Unsigned Integer16 type. + */ + value.set(static_cast<uint16_t>(42)); + if (value.isNone() || value.isArray() || + value.type() != ControlTypeUnsigned16) { + cerr << "Control type mismatch after setting to uint16_t" << endl; + return TestFail; + } + + if (value.get<uint16_t>() != 42) { + cerr << "Control value mismatch after setting to uint16_t" << endl; + return TestFail; + } + + if (value.toString() != "42") { + cerr << "Control string mismatch after setting to uint16_t" << endl; + return TestFail; + } + + std::array<uint16_t, 4> uint16s{ 3, 14, 15, 9 }; + value.set(Span<uint16_t>(uint16s)); + if (value.isNone() || !value.isArray() || + value.type() != ControlTypeUnsigned16) { + cerr << "Control type mismatch after setting to uint16_t array" << endl; + return TestFail; + } + + Span<const uint16_t> uint16sResult = value.get<Span<const uint16_t>>(); + if (uint16s.size() != uint16sResult.size() || + !std::equal(uint16s.begin(), uint16s.end(), uint16sResult.begin())) { + cerr << "Control value mismatch after setting to uint16_t array" << endl; + return TestFail; + } + + if (value.toString() != "[ 3, 14, 15, 9 ]") { + cerr << "Control string mismatch after setting to uint16_t array" << endl; + return TestFail; + } + + /* + * Unsigned Integer32 type. + */ + value.set(static_cast<uint32_t>(42)); + if (value.isNone() || value.isArray() || + value.type() != ControlTypeUnsigned32) { + cerr << "Control type mismatch after setting to uint32_t" << endl; + return TestFail; + } + + if (value.get<uint32_t>() != 42) { + cerr << "Control value mismatch after setting to uint32_t" << endl; + return TestFail; + } + + if (value.toString() != "42") { + cerr << "Control string mismatch after setting to uint32_t" << endl; + return TestFail; + } + + std::array<uint32_t, 4> uint32s{ 3, 14, 15, 9 }; + value.set(Span<uint32_t>(uint32s)); + if (value.isNone() || !value.isArray() || + value.type() != ControlTypeUnsigned32) { + cerr << "Control type mismatch after setting to uint32_t array" << endl; + return TestFail; + } + + Span<const uint32_t> uint32sResult = value.get<Span<const uint32_t>>(); + if (uint32s.size() != uint32sResult.size() || + !std::equal(uint32s.begin(), uint32s.end(), uint32sResult.begin())) { + cerr << "Control value mismatch after setting to uint32_t array" << endl; + return TestFail; + } + + if (value.toString() != "[ 3, 14, 15, 9 ]") { + cerr << "Control string mismatch after setting to uint32_t array" << endl; + return TestFail; + } + + /* * Integer32 type. */ value.set(0x42000000); diff --git a/test/controls/meson.build b/test/controls/meson.build index 7fff2413..763f8905 100644 --- a/test/controls/meson.build +++ b/test/controls/meson.build @@ -1,14 +1,16 @@ +# SPDX-License-Identifier: CC0-1.0 + control_tests = [ - [ 'control_info', 'control_info.cpp' ], - [ 'control_info_map', 'control_info_map.cpp' ], - [ 'control_list', 'control_list.cpp' ], - [ 'control_value', 'control_value.cpp' ], + {'name': 'control_info', 'sources': ['control_info.cpp']}, + {'name': 'control_info_map', 'sources': ['control_info_map.cpp']}, + {'name': 'control_list', 'sources': ['control_list.cpp']}, + {'name': 'control_value', 'sources': ['control_value.cpp']}, ] -foreach t : control_tests - exe = executable(t[0], t[1], - dependencies : libcamera_dep, +foreach test : control_tests + exe = executable(test['name'], test['sources'], + dependencies : libcamera_public, link_with : test_libraries, include_directories : test_includes_internal) - test(t[0], exe, suite : 'controls', is_parallel : false) + test(test['name'], exe, suite : 'controls', is_parallel : false) endforeach diff --git a/test/delayed_controls.cpp b/test/delayed_controls.cpp new file mode 100644 index 00000000..7bd30e7a --- /dev/null +++ b/test/delayed_controls.cpp @@ -0,0 +1,303 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Google Inc. + * + * libcamera delayed controls test + */ + +#include <iostream> + +#include "libcamera/internal/delayed_controls.h" +#include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/media_device.h" +#include "libcamera/internal/v4l2_videodevice.h" + +#include "test.h" + +using namespace std; +using namespace libcamera; + +class DelayedControlsTest : public Test +{ +public: + DelayedControlsTest() + { + } + +protected: + int init() override + { + enumerator_ = DeviceEnumerator::create(); + if (!enumerator_) { + cerr << "Failed to create device enumerator" << endl; + return TestFail; + } + + if (enumerator_->enumerate()) { + cerr << "Failed to enumerate media devices" << endl; + return TestFail; + } + + DeviceMatch dm("vivid"); + dm.add("vivid-000-vid-cap"); + + media_ = enumerator_->search(dm); + if (!media_) { + cerr << "vivid video device found" << endl; + return TestSkip; + } + + dev_ = V4L2VideoDevice::fromEntityName(media_.get(), "vivid-000-vid-cap"); + if (dev_->open()) { + cerr << "Failed to open video device" << endl; + return TestFail; + } + + const ControlInfoMap &infoMap = dev_->controls(); + + /* Make sure the controls we require are present. */ + if (infoMap.empty()) { + cerr << "Failed to enumerate controls" << endl; + return TestFail; + } + + if (infoMap.find(V4L2_CID_BRIGHTNESS) == infoMap.end() || + infoMap.find(V4L2_CID_CONTRAST) == infoMap.end()) { + cerr << "Missing controls" << endl; + return TestFail; + } + + return TestPass; + } + + int singleControlNoDelay() + { + std::unordered_map<uint32_t, DelayedControls::ControlParams> delays = { + { V4L2_CID_BRIGHTNESS, { 0, false } }, + }; + std::unique_ptr<DelayedControls> delayed = + std::make_unique<DelayedControls>(dev_.get(), delays); + ControlList ctrls; + + /* Reset control to value not used in test. */ + ctrls.set(V4L2_CID_BRIGHTNESS, 1); + dev_->setControls(&ctrls); + delayed->reset(); + + /* Trigger the first frame start event */ + delayed->applyControls(0); + + /* Test control without delay are set at once. */ + for (unsigned int i = 1; i < 100; i++) { + int32_t value = 100 + i; + + ctrls.set(V4L2_CID_BRIGHTNESS, value); + delayed->push(ctrls); + + delayed->applyControls(i); + + ControlList result = delayed->get(i); + int32_t brightness = result.get(V4L2_CID_BRIGHTNESS).get<int32_t>(); + if (brightness != value) { + cerr << "Failed single control without delay" + << " frame " << i + << " expected " << value + << " got " << brightness + << endl; + return TestFail; + } + } + + return TestPass; + } + + int singleControlWithDelay() + { + std::unordered_map<uint32_t, DelayedControls::ControlParams> delays = { + { V4L2_CID_BRIGHTNESS, { 1, false } }, + }; + std::unique_ptr<DelayedControls> delayed = + std::make_unique<DelayedControls>(dev_.get(), delays); + ControlList ctrls; + + /* Reset control to value that will be first in test. */ + int32_t expected = 4; + ctrls.set(V4L2_CID_BRIGHTNESS, expected); + dev_->setControls(&ctrls); + delayed->reset(); + + /* Trigger the first frame start event */ + delayed->applyControls(0); + + /* Test single control with delay. */ + for (unsigned int i = 1; i < 100; i++) { + int32_t value = 10 + i; + + ctrls.set(V4L2_CID_BRIGHTNESS, value); + delayed->push(ctrls); + + delayed->applyControls(i); + + ControlList result = delayed->get(i); + int32_t brightness = result.get(V4L2_CID_BRIGHTNESS).get<int32_t>(); + if (brightness != expected) { + cerr << "Failed single control with delay" + << " frame " << i + << " expected " << expected + << " got " << brightness + << endl; + return TestFail; + } + + expected = value; + } + + return TestPass; + } + + int dualControlsWithDelay() + { + static const unsigned int maxDelay = 2; + + std::unordered_map<uint32_t, DelayedControls::ControlParams> delays = { + { V4L2_CID_BRIGHTNESS, { 1, false } }, + { V4L2_CID_CONTRAST, { maxDelay, false } }, + }; + std::unique_ptr<DelayedControls> delayed = + std::make_unique<DelayedControls>(dev_.get(), delays); + ControlList ctrls; + + /* Reset control to value that will be first two frames in test. */ + int32_t expected = 200; + ctrls.set(V4L2_CID_BRIGHTNESS, expected); + ctrls.set(V4L2_CID_CONTRAST, expected + 1); + dev_->setControls(&ctrls); + delayed->reset(); + + /* Trigger the first frame start event */ + delayed->applyControls(0); + + /* Test dual control with delay. */ + for (unsigned int i = 1; i < 100; i++) { + int32_t value = 10 + i; + + ctrls.set(V4L2_CID_BRIGHTNESS, value); + ctrls.set(V4L2_CID_CONTRAST, value + 1); + delayed->push(ctrls); + + delayed->applyControls(i); + + ControlList result = delayed->get(i); + int32_t brightness = result.get(V4L2_CID_BRIGHTNESS).get<int32_t>(); + int32_t contrast = result.get(V4L2_CID_CONTRAST).get<int32_t>(); + if (brightness != expected || contrast != expected + 1) { + cerr << "Failed dual controls" + << " frame " << i + << " brightness " << brightness + << " contrast " << contrast + << " expected " << expected + << endl; + return TestFail; + } + + expected = i < maxDelay ? expected : value - 1; + } + + return TestPass; + } + + int dualControlsMultiQueue() + { + static const unsigned int maxDelay = 2; + + std::unordered_map<uint32_t, DelayedControls::ControlParams> delays = { + { V4L2_CID_BRIGHTNESS, { 1, false } }, + { V4L2_CID_CONTRAST, { maxDelay, false } } + }; + std::unique_ptr<DelayedControls> delayed = + std::make_unique<DelayedControls>(dev_.get(), delays); + ControlList ctrls; + + /* Reset control to value that will be first two frames in test. */ + int32_t expected = 100; + ctrls.set(V4L2_CID_BRIGHTNESS, expected); + ctrls.set(V4L2_CID_CONTRAST, expected); + dev_->setControls(&ctrls); + delayed->reset(); + + /* Trigger the first frame start event */ + delayed->applyControls(0); + + /* + * Queue all controls before any fake frame start. Note we + * can't queue up more then the delayed controls history size + * which is 16. Where one spot is used by the reset control. + */ + for (unsigned int i = 0; i < 15; i++) { + int32_t value = 10 + i; + + ctrls.set(V4L2_CID_BRIGHTNESS, value); + ctrls.set(V4L2_CID_CONTRAST, value); + delayed->push(ctrls); + } + + /* Process all queued controls. */ + for (unsigned int i = 1; i < 16; i++) { + int32_t value = 10 + i - 1; + + delayed->applyControls(i); + + ControlList result = delayed->get(i); + + int32_t brightness = result.get(V4L2_CID_BRIGHTNESS).get<int32_t>(); + int32_t contrast = result.get(V4L2_CID_CONTRAST).get<int32_t>(); + if (brightness != expected || contrast != expected) { + cerr << "Failed multi queue" + << " frame " << i + << " brightness " << brightness + << " contrast " << contrast + << " expected " << expected + << endl; + return TestFail; + } + + expected = i < maxDelay ? expected : value - 1; + } + + return TestPass; + } + + int run() override + { + int ret; + + /* Test single control without delay. */ + ret = singleControlNoDelay(); + if (ret) + return ret; + + /* Test single control with delay. */ + ret = singleControlWithDelay(); + if (ret) + return ret; + + /* Test dual controls with different delays. */ + ret = dualControlsWithDelay(); + if (ret) + return ret; + + /* Test control values produced faster than consumed. */ + ret = dualControlsMultiQueue(); + if (ret) + return ret; + + return TestPass; + } + +private: + std::unique_ptr<DeviceEnumerator> enumerator_; + std::shared_ptr<MediaDevice> media_; + std::unique_ptr<V4L2VideoDevice> dev_; +}; + +TEST_REGISTER(DelayedControlsTest) diff --git a/test/event-dispatcher.cpp b/test/event-dispatcher.cpp index 9f9cf178..f71c1c0d 100644 --- a/test/event-dispatcher.cpp +++ b/test/event-dispatcher.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * event-dispatcher.cpp - Event dispatcher test + * Event dispatcher test */ #include <chrono> @@ -10,14 +10,15 @@ #include <signal.h> #include <sys/time.h> -#include <libcamera/event_dispatcher.h> -#include <libcamera/timer.h> +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> #include "test.h" -#include "thread.h" -using namespace std; using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; static EventDispatcher *dispatcher; static bool interrupt; @@ -50,7 +51,7 @@ protected: /* Event processing interruption by signal. */ std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); - timer.start(1000); + timer.start(1000ms); struct itimerval itimer = {}; itimer.it_value.tv_usec = 500000; @@ -69,7 +70,7 @@ protected: } /* Event processing interruption. */ - timer.start(1000); + timer.start(1000ms); dispatcher->interrupt(); dispatcher->processEvents(); @@ -79,7 +80,7 @@ protected: return TestFail; } - timer.start(1000); + timer.start(1000ms); itimer.it_value.tv_usec = 500000; interrupt = true; setitimer(ITIMER_REAL, &itimer, nullptr); diff --git a/test/event-thread.cpp b/test/event-thread.cpp index 01120733..5499bbf8 100644 --- a/test/event-thread.cpp +++ b/test/event-thread.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * event-thread.cpp - Threaded event test + * Threaded event test */ #include <chrono> @@ -10,11 +10,12 @@ #include <string.h> #include <unistd.h> -#include <libcamera/event_notifier.h> -#include <libcamera/timer.h> +#include <libcamera/base/event_notifier.h> +#include <libcamera/base/object.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> #include "test.h" -#include "thread.h" using namespace std; using namespace libcamera; @@ -66,9 +67,9 @@ public: } private: - void readReady(EventNotifier *notifier) + void readReady() { - size_ = read(notifier->fd(), data_, sizeof(data_)); + size_ = read(notifier_->fd(), data_, sizeof(data_)); notified_ = true; } @@ -84,10 +85,17 @@ private: class EventThreadTest : public Test { protected: + int init() + { + thread_.start(); + + handler_ = new EventHandler(); + + return TestPass; + } + int run() { - Thread thread; - thread.start(); /* * Fire the event notifier and then move the notifier to a @@ -97,23 +105,33 @@ protected: * different thread will correctly process already pending * events in the new thread. */ - EventHandler handler; - handler.notify(); - handler.moveToThread(&thread); + handler_->notify(); + handler_->moveToThread(&thread_); this_thread::sleep_for(chrono::milliseconds(100)); - /* Must stop thread before destroying the handler. */ - thread.exit(0); - thread.wait(); - - if (!handler.notified()) { + if (!handler_->notified()) { cout << "Thread event handling test failed" << endl; return TestFail; } return TestPass; } + + void cleanup() + { + /* + * Object class instances must be destroyed from the thread + * they live in. + */ + handler_->deleteLater(); + thread_.exit(0); + thread_.wait(); + } + +private: + EventHandler *handler_; + Thread thread_; }; TEST_REGISTER(EventThreadTest) diff --git a/test/event.cpp b/test/event.cpp index 816060cc..9f7b1ed4 100644 --- a/test/event.cpp +++ b/test/event.cpp @@ -2,34 +2,37 @@ /* * Copyright (C) 2019, Google Inc. * - * event.cpp - Event test + * Event test */ #include <iostream> #include <string.h> #include <unistd.h> -#include <libcamera/event_dispatcher.h> -#include <libcamera/event_notifier.h> -#include <libcamera/timer.h> +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/event_notifier.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> #include "test.h" -#include "thread.h" -using namespace std; using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; class EventTest : public Test { protected: - void readReady(EventNotifier *notifier) + void readReady() { - size_ = read(notifier->fd(), data_, sizeof(data_)); + size_ = read(notifier_->fd(), data_, sizeof(data_)); notified_ = true; } int init() { + notifier_ = nullptr; + return pipe(pipefd_); } @@ -40,8 +43,8 @@ protected: Timer timeout; ssize_t ret; - EventNotifier readNotifier(pipefd_[0], EventNotifier::Read); - readNotifier.activated.connect(this, &EventTest::readReady); + notifier_ = new EventNotifier(pipefd_[0], EventNotifier::Read); + notifier_->activated.connect(this, &EventTest::readReady); /* Test read notification with data. */ memset(data_, 0, sizeof(data_)); @@ -53,7 +56,7 @@ protected: return TestFail; } - timeout.start(100); + timeout.start(100ms); dispatcher->processEvents(); timeout.stop(); @@ -65,7 +68,7 @@ protected: /* Test read notification without data. */ notified_ = false; - timeout.start(100); + timeout.start(100ms); dispatcher->processEvents(); timeout.stop(); @@ -76,7 +79,7 @@ protected: /* Test read notifier disabling. */ notified_ = false; - readNotifier.setEnabled(false); + notifier_->setEnabled(false); ret = write(pipefd_[1], data.data(), data.size()); if (ret < 0) { @@ -84,7 +87,7 @@ protected: return TestFail; } - timeout.start(100); + timeout.start(100ms); dispatcher->processEvents(); timeout.stop(); @@ -95,9 +98,9 @@ protected: /* Test read notifier enabling. */ notified_ = false; - readNotifier.setEnabled(true); + notifier_->setEnabled(true); - timeout.start(100); + timeout.start(100ms); dispatcher->processEvents(); timeout.stop(); @@ -111,6 +114,8 @@ protected: void cleanup() { + delete notifier_; + close(pipefd_[0]); close(pipefd_[1]); } @@ -118,6 +123,7 @@ protected: private: int pipefd_[2]; + EventNotifier *notifier_; bool notified_; char data_[16]; ssize_t size_; diff --git a/test/fence.cpp b/test/fence.cpp new file mode 100644 index 00000000..8095b228 --- /dev/null +++ b/test/fence.cpp @@ -0,0 +1,361 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Google Inc. + * + * Fence test + */ + +#include <iostream> +#include <memory> +#include <sys/eventfd.h> +#include <unistd.h> + +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> +#include <libcamera/base/unique_fd.h> +#include <libcamera/base/utils.h> + +#include <libcamera/fence.h> +#include <libcamera/framebuffer_allocator.h> + +#include "camera_test.h" +#include "test.h" + +using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; + +class FenceTest : public CameraTest, public Test +{ +public: + FenceTest(); + +protected: + int init() override; + int run() override; + +private: + int validateExpiredRequest(Request *request); + int validateRequest(Request *request); + void requestComplete(Request *request); + void requestRequeue(Request *request); + + void signalFence(); + + EventDispatcher *dispatcher_; + UniqueFD eventFd_; + UniqueFD eventFd2_; + Timer fenceTimer_; + + std::vector<std::unique_ptr<Request>> requests_; + std::unique_ptr<CameraConfiguration> config_; + std::unique_ptr<FrameBufferAllocator> allocator_; + + Stream *stream_; + + bool expectedCompletionResult_ = true; + bool setFence_ = true; + + /* + * Request IDs track the number of requests that have completed. They + * are one-based, and don't wrap. + */ + unsigned int completedRequestId_; + unsigned int signalledRequestId_; + unsigned int expiredRequestId_; + unsigned int nbuffers_; + + int efd2_; + int efd_; +}; + +FenceTest::FenceTest() + : CameraTest("platform/vimc.0 Sensor B") +{ +} + +int FenceTest::init() +{ + /* Make sure the CameraTest constructor succeeded. */ + if (status_ != TestPass) + return status_; + + dispatcher_ = Thread::current()->eventDispatcher(); + + /* + * Create two eventfds to model the fences. This is enough to support the + * needs of libcamera which only needs to wait for read events through + * poll(). Once native support for fences will be available in the + * backend kernel APIs this will need to be replaced by a sw_sync fence, + * but that requires debugfs. + */ + eventFd_ = UniqueFD(eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK)); + eventFd2_ = UniqueFD(eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK)); + if (!eventFd_.isValid() || !eventFd2_.isValid()) { + cerr << "Unable to create eventfd" << endl; + return TestFail; + } + + efd_ = eventFd_.get(); + efd2_ = eventFd2_.get(); + + config_ = camera_->generateConfiguration({ StreamRole::Viewfinder }); + if (!config_ || config_->size() != 1) { + cerr << "Failed to generate default configuration" << endl; + return TestFail; + } + + if (camera_->acquire()) { + cerr << "Failed to acquire the camera" << endl; + return TestFail; + } + + if (camera_->configure(config_.get())) { + cerr << "Failed to set default configuration" << endl; + return TestFail; + } + + StreamConfiguration &cfg = config_->at(0); + stream_ = cfg.stream(); + + allocator_ = std::make_unique<FrameBufferAllocator>(camera_); + if (allocator_->allocate(stream_) < 0) + return TestFail; + + nbuffers_ = allocator_->buffers(stream_).size(); + if (nbuffers_ < 2) { + cerr << "Not enough buffers available" << endl; + return TestFail; + } + + completedRequestId_ = 0; + + /* + * All but two requests are queued without a fence. Request + * expiredRequestId_ will be queued with a fence that we won't signal + * (which is then expected to expire), and request signalledRequestId_ + * will be queued with a fence that gets signalled. Select nbuffers_ + * and nbuffers_ * 2 for those two requests, to space them by a few + * frames while still not requiring a long time for the test to + * complete. + */ + expiredRequestId_ = nbuffers_; + signalledRequestId_ = nbuffers_ * 2; + + return TestPass; +} + +int FenceTest::validateExpiredRequest(Request *request) +{ + /* The last request is expected to fail. */ + if (request->status() != Request::RequestCancelled) { + cerr << "The last request should have failed: " << endl; + return TestFail; + } + + FrameBuffer *buffer = request->buffers().begin()->second; + std::unique_ptr<Fence> fence = buffer->releaseFence(); + if (!fence) { + cerr << "The expired fence should be present" << endl; + return TestFail; + } + + if (!fence->isValid()) { + cerr << "The expired fence should be valid" << endl; + return TestFail; + } + + UniqueFD fd = fence->release(); + if (fd.get() != efd_) { + cerr << "The expired fence file descriptor should not change" << endl; + return TestFail; + } + + return TestPass; +} + +int FenceTest::validateRequest(Request *request) +{ + uint64_t cookie = request->cookie(); + + /* All requests but the last are expected to succeed. */ + if (request->status() != Request::RequestComplete) { + cerr << "Unexpected request failure: " << cookie << endl; + return TestFail; + } + + /* A successfully completed request should have the Fence closed. */ + const Request::BufferMap &buffers = request->buffers(); + FrameBuffer *buffer = buffers.begin()->second; + + std::unique_ptr<Fence> bufferFence = buffer->releaseFence(); + if (bufferFence) { + cerr << "Unexpected valid fence in completed request" << endl; + return TestFail; + } + + return TestPass; +} + +void FenceTest::requestRequeue(Request *request) +{ + const Request::BufferMap &buffers = request->buffers(); + const Stream *stream = buffers.begin()->first; + FrameBuffer *buffer = buffers.begin()->second; + + request->reuse(); + + if (completedRequestId_ == signalledRequestId_ - nbuffers_ && setFence_) { + /* + * This is the request that will be used to test fence + * signalling when it completes next time. Add a fence to it, + * using efd2_. The main loop will signal the fence by using a + * timer to write to the efd2_ file descriptor before the fence + * expires. + */ + std::unique_ptr<Fence> fence = + std::make_unique<Fence>(std::move(eventFd2_)); + request->addBuffer(stream, buffer, std::move(fence)); + } else { + /* All the other requests continue to operate without fences. */ + request->addBuffer(stream, buffer); + } + + camera_->queueRequest(request); +} + +void FenceTest::requestComplete(Request *request) +{ + completedRequestId_++; + + /* + * Request expiredRequestId_ is expected to fail as its fence has not + * been signalled. + * + * Validate the fence status but do not re-queue it. + */ + if (completedRequestId_ == expiredRequestId_) { + if (validateExpiredRequest(request) != TestPass) + expectedCompletionResult_ = false; + + dispatcher_->interrupt(); + return; + } + + /* Validate all other requests. */ + if (validateRequest(request) != TestPass) { + expectedCompletionResult_ = false; + + dispatcher_->interrupt(); + return; + } + + requestRequeue(request); + + /* + * Interrupt the dispatcher to return control to the main loop and + * activate the fenceTimer. + */ + dispatcher_->interrupt(); +} + +/* Callback to signal a fence waiting on the eventfd file descriptor. */ +void FenceTest::signalFence() +{ + uint64_t value = 1; + int ret; + + ret = write(efd2_, &value, sizeof(value)); + if (ret != sizeof(value)) + cerr << "Failed to signal fence" << endl; + + setFence_ = false; + dispatcher_->processEvents(); +} + +int FenceTest::run() +{ + for (const auto &[i, buffer] : utils::enumerate(allocator_->buffers(stream_))) { + std::unique_ptr<Request> request = camera_->createRequest(i); + if (!request) { + cerr << "Failed to create request" << endl; + return TestFail; + } + + int ret; + if (i == expiredRequestId_ - 1) { + /* This request will have a fence, and it will expire. */ + std::unique_ptr<Fence> fence = + std::make_unique<Fence>(std::move(eventFd_)); + if (!fence->isValid()) { + cerr << "Fence should be valid" << endl; + return TestFail; + } + + ret = request->addBuffer(stream_, buffer.get(), std::move(fence)); + } else { + /* All other requests will have no Fence. */ + ret = request->addBuffer(stream_, buffer.get()); + } + + if (ret) { + cerr << "Failed to associate buffer with request" << endl; + return TestFail; + } + + requests_.push_back(std::move(request)); + } + + camera_->requestCompleted.connect(this, &FenceTest::requestComplete); + + if (camera_->start()) { + cerr << "Failed to start camera" << endl; + return TestFail; + } + + for (std::unique_ptr<Request> &request : requests_) { + if (camera_->queueRequest(request.get())) { + cerr << "Failed to queue request" << endl; + return TestFail; + } + } + + expectedCompletionResult_ = true; + + /* This timer serves to signal fences associated with "signalledRequestId_" */ + Timer fenceTimer; + fenceTimer.timeout.connect(this, &FenceTest::signalFence); + + /* + * Loop long enough for all requests to complete, allowing 500ms per + * request. + */ + Timer timer; + timer.start(500ms * (signalledRequestId_ + 1)); + while (timer.isRunning() && expectedCompletionResult_ && + completedRequestId_ <= signalledRequestId_ + 1) { + if (completedRequestId_ == signalledRequestId_ - 1 && setFence_) + /* + * The request just before signalledRequestId_ has just + * completed. Request signalledRequestId_ has been + * queued with a fence, and libcamera is likely already + * waiting on the fence, or will soon. Start the timer + * to signal the fence in 10 msec. + */ + fenceTimer.start(10ms); + + dispatcher_->processEvents(); + } + + camera_->requestCompleted.disconnect(); + + if (camera_->stop()) { + cerr << "Failed to stop camera" << endl; + return TestFail; + } + + return expectedCompletionResult_ ? TestPass : TestFail; +} + +TEST_REGISTER(FenceTest) diff --git a/test/file.cpp b/test/file.cpp new file mode 100644 index 00000000..170e6ccd --- /dev/null +++ b/test/file.cpp @@ -0,0 +1,383 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Google Inc. + * + * File I/O operations tests + */ + +#include <fstream> +#include <iostream> +#include <stdlib.h> +#include <string> +#include <string.h> +#include <sys/types.h> +#include <unistd.h> + +#include <libcamera/base/file.h> + +#include "test.h" + +using namespace std; +using namespace libcamera; + +class FileTest : public Test +{ +protected: + int init() + { + fileName_ = "/tmp/libcamera.test.XXXXXX"; + int fd = mkstemp(&fileName_.front()); + if (fd == -1) + return TestFail; + + close(fd); + unlink(fileName_.c_str()); + + return TestPass; + } + + int run() + { + /* Test static functions. */ + if (!File::exists("/dev/null")) { + cerr << "Valid file not found" << endl; + return TestFail; + } + + if (File::exists("/dev/null/invalid")) { + cerr << "Invalid file should not exist" << endl; + return TestFail; + } + + if (File::exists("/dev")) { + cerr << "Directories should not be treated as files" << endl; + return TestFail; + } + + /* Test unnamed file. */ + File file; + + if (!file.fileName().empty()) { + cerr << "Unnamed file has non-empty file name" << endl; + return TestFail; + } + + if (file.exists()) { + cerr << "Unnamed file exists" << endl; + return TestFail; + } + + if (file.isOpen()) { + cerr << "File is open after construction" << endl; + return TestFail; + } + + if (file.openMode() != File::OpenModeFlag::NotOpen) { + cerr << "File has invalid open mode after construction" + << endl; + return TestFail; + } + + if (file.size() >= 0) { + cerr << "Unnamed file has a size" << endl; + return TestFail; + } + + if (file.open(File::OpenModeFlag::ReadWrite)) { + cerr << "Opening unnamed file succeeded" << endl; + return TestFail; + } + + if (file.error() == 0) { + cerr << "Open failure didn't set error" << endl; + return TestFail; + } + + /* Test named file referring to an invalid file. */ + file.setFileName("/dev/null/invalid"); + + if (file.fileName() != "/dev/null/invalid") { + cerr << "File reports incorrect file name" << endl; + return TestFail; + } + + if (file.exists()) { + cerr << "Invalid file exists" << endl; + return TestFail; + } + + if (file.isOpen()) { + cerr << "Invalid file is open after construction" << endl; + return TestFail; + } + + if (file.openMode() != File::OpenModeFlag::NotOpen) { + cerr << "Invalid file has invalid open mode after construction" + << endl; + return TestFail; + } + + if (file.size() >= 0) { + cerr << "Invalid file has a size" << endl; + return TestFail; + } + + if (file.open(File::OpenModeFlag::ReadWrite)) { + cerr << "Opening invalid file succeeded" << endl; + return TestFail; + } + + /* Test named file referring to a valid file. */ + file.setFileName("/dev/null"); + + if (!file.exists()) { + cerr << "Valid file does not exist" << endl; + return TestFail; + } + + if (file.isOpen()) { + cerr << "Valid file is open after construction" << endl; + return TestFail; + } + + if (file.openMode() != File::OpenModeFlag::NotOpen) { + cerr << "Valid file has invalid open mode after construction" + << endl; + return TestFail; + } + + if (file.size() >= 0) { + cerr << "Invalid file has a size" << endl; + return TestFail; + } + + /* Test open and close. */ + if (!file.open(File::OpenModeFlag::ReadWrite)) { + cerr << "Opening file failed" << endl; + return TestFail; + } + + if (!file.isOpen()) { + cerr << "Open file reported as closed" << endl; + return TestFail; + } + + if (file.openMode() != File::OpenModeFlag::ReadWrite) { + cerr << "Open file has invalid open mode" << endl; + return TestFail; + } + + file.close(); + + if (file.isOpen()) { + cerr << "Closed file reported as open" << endl; + return TestFail; + } + + if (file.openMode() != File::OpenModeFlag::NotOpen) { + cerr << "Closed file has invalid open mode" << endl; + return TestFail; + } + + /* Test size(). */ + file.setFileName(self()); + + if (file.size() >= 0) { + cerr << "File has valid size before open" << endl; + return TestFail; + } + + file.open(File::OpenModeFlag::ReadOnly); + + ssize_t size = file.size(); + if (size <= 0) { + cerr << "File has invalid size after open" << endl; + return TestFail; + } + + file.close(); + + /* Test file creation. */ + file.setFileName(fileName_); + + if (file.exists()) { + cerr << "Temporary file already exists" << endl; + return TestFail; + } + + if (file.open(File::OpenModeFlag::ReadOnly)) { + cerr << "Read-only open succeeded on nonexistent file" << endl; + return TestFail; + } + + if (!file.open(File::OpenModeFlag::WriteOnly)) { + cerr << "Write-only open failed on nonexistent file" << endl; + return TestFail; + } + + if (!file.exists()) { + cerr << "Write-only open failed to create file" << endl; + return TestFail; + } + + file.close(); + + /* Test read and write. */ + std::array<uint8_t, 256> buffer = { 0 }; + + strncpy(reinterpret_cast<char *>(buffer.data()), "libcamera", + buffer.size()); + + if (file.read(buffer) >= 0) { + cerr << "Read succeeded on closed file" << endl; + return TestFail; + } + + if (file.write(buffer) >= 0) { + cerr << "Write succeeded on closed file" << endl; + return TestFail; + } + + file.open(File::OpenModeFlag::ReadOnly); + + if (file.write(buffer) >= 0) { + cerr << "Write succeeded on read-only file" << endl; + return TestFail; + } + + file.close(); + + file.open(File::OpenModeFlag::ReadWrite); + + if (file.write({ buffer.data(), 9 }) != 9) { + cerr << "Write test failed" << endl; + return TestFail; + } + + if (file.read(buffer) != 0) { + cerr << "Read at end of file test failed" << endl; + return TestFail; + } + + if (file.seek(0) != 0) { + cerr << "Seek test failed" << endl; + return TestFail; + } + + if (file.read(buffer) != 9) { + cerr << "Read test failed" << endl; + return TestFail; + } + + if (file.pos() != 9) { + cerr << "Position test failed" << endl; + return TestFail; + } + + file.close(); + + /* Test mapping and unmapping. */ + file.setFileName(self()); + file.open(File::OpenModeFlag::ReadOnly); + + Span<uint8_t> data = file.map(); + if (data.empty()) { + cerr << "Mapping of complete file failed" << endl; + return TestFail; + } + + if (data.size() != static_cast<size_t>(size)) { + cerr << "Mapping of complete file has invalid size" << endl; + return TestFail; + } + + if (!file.unmap(data.data())) { + cerr << "Unmapping of complete file failed" << endl; + return TestFail; + } + + data = file.map(4096, 8192); + if (data.empty()) { + cerr << "Mapping of file region failed" << endl; + return TestFail; + } + + if (data.size() != 8192) { + cerr << "Mapping of file region has invalid size" << endl; + return TestFail; + } + + if (!file.unmap(data.data())) { + cerr << "Unmapping of file region failed" << endl; + return TestFail; + } + + file.close(); + + /* Test private mapping. */ + file.setFileName(fileName_); + file.open(File::OpenModeFlag::ReadWrite); + + data = file.map(0, -1, File::MapFlag::Private); + if (data.empty()) { + cerr << "Private mapping failed" << endl; + return TestFail; + } + + std::string str{ reinterpret_cast<char *>(data.data()), data.size() }; + if (str != "libcamera") { + cerr << "Invalid contents of private mapping" << endl; + return TestFail; + } + + memcpy(data.data(), "LIBCAMERA", 9); + + if (!file.unmap(data.data())) { + cerr << "Private unmapping failed" << endl; + return TestFail; + } + + data = file.map(); + + str = { reinterpret_cast<char *>(data.data()), data.size() }; + if (str != "libcamera") { + cerr << "Private mapping changed file contents" << endl; + return TestFail; + } + + /* Test shared mapping. */ + data = file.map(); + if (data.empty()) { + cerr << "Shared mapping failed" << endl; + return TestFail; + } + + memcpy(data.data(), "LIBCAMERA", 9); + + if (!file.unmap(data.data())) { + cerr << "Shared unmapping failed" << endl; + return TestFail; + } + + data = file.map(); + + str = { reinterpret_cast<char *>(data.data()), data.size() }; + if (str != "LIBCAMERA") { + cerr << "Shared mapping failed to change file contents" + << endl; + return TestFail; + } + + return TestPass; + } + + void cleanup() + { + unlink(fileName_.c_str()); + } + +private: + std::string fileName_; +}; + +TEST_REGISTER(FileTest) diff --git a/test/flags.cpp b/test/flags.cpp new file mode 100644 index 00000000..85c34788 --- /dev/null +++ b/test/flags.cpp @@ -0,0 +1,193 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Google Inc. + * + * Flags tests + */ + +#include <iostream> + +#include <libcamera/base/flags.h> + +#include "test.h" + +using namespace libcamera; +using namespace std; + +class FlagsTest : public Test +{ +protected: + enum class Option { + First = (1 << 0), + Second = (1 << 1), + Third = (1 << 2), + }; + + using Options = Flags<Option>; + + enum class Mode { + Alpha = (1 << 0), + Beta = (1 << 1), + Gamma = (1 << 2), + }; + + using Modes = Flags<Mode>; + + int run() override; +}; + +namespace libcamera { + +LIBCAMERA_FLAGS_ENABLE_OPERATORS(FlagsTest::Option) + +} /* namespace libcamera */ + +int FlagsTest::run() +{ + /* Commented-out constructs are expected not to compile. */ + + /* Flags<int> f; */ + + /* + * Unary operators with enum argument. + */ + + Options options; + + if (options) { + cerr << "Default-constructed Flags<> is not zero" << endl; + return TestFail; + } + + options |= Option::First; + if (!options || options != Option::First) { + cerr << "Unary bitwise OR with enum failed" << endl; + return TestFail; + } + + /* options &= Mode::Alpha; */ + /* options |= Mode::Beta; */ + /* options ^= Mode::Gamma; */ + + options &= ~Option::First; + if (options) { + cerr << "Unary bitwise AND with enum failed" << endl; + return TestFail; + } + + options ^= Option::Second; + if (options != Option::Second) { + cerr << "Unary bitwise XOR with enum failed" << endl; + return TestFail; + } + + options = Options(); + + /* + * Unary operators with Flags argument. + */ + + options |= Options(Option::First); + if (options != Option::First) { + cerr << "Unary bitwise OR with Flags<> failed" << endl; + return TestFail; + } + + /* options &= Options(Mode::Alpha); */ + /* options |= Options(Mode::Beta); */ + /* options ^= Options(Mode::Gamma); */ + + options &= ~Options(Option::First); + if (options) { + cerr << "Unary bitwise AND with Flags<> failed" << endl; + return TestFail; + } + + options ^= Options(Option::Second); + if (options != Option::Second) { + cerr << "Unary bitwise XOR with Flags<> failed" << endl; + return TestFail; + } + + options = Options(); + + /* + * Binary operators with enum argument. + */ + + options = options | Option::First; + if (!(options & Option::First)) { + cerr << "Binary bitwise OR with enum failed" << endl; + return TestFail; + } + + /* options = options & Mode::Alpha; */ + /* options = options | Mode::Beta; */ + /* options = options ^ Mode::Gamma; */ + + options = options & ~Option::First; + if (options != (Option::First & Option::Second)) { + cerr << "Binary bitwise AND with enum failed" << endl; + return TestFail; + } + + options = options ^ (Option::First ^ Option::Second); + if (options != (Option::First | Option::Second)) { + cerr << "Binary bitwise XOR with enum failed" << endl; + return TestFail; + } + + options = Options(); + + /* + * Binary operators with Flags argument. + */ + + options |= Options(Option::First); + if (!(options & Option::First)) { + cerr << "Binary bitwise OR with Flags<> failed" << endl; + return TestFail; + } + + /* options = options & Options(Mode::Alpha); */ + /* options = options | Options(Mode::Beta); */ + /* options = options ^ Options(Mode::Gamma); */ + + options = options & ~Options(Option::First); + if (options) { + cerr << "Binary bitwise AND with Flags<> failed" << endl; + return TestFail; + } + + options = options ^ Options(Option::Second); + if (options != Option::Second) { + cerr << "Binary bitwise XOR with Flags<> failed" << endl; + return TestFail; + } + + options = Options(); + + /* + * Conversion operators. + */ + + options |= Option::First | Option::Second | Option::Third; + if (static_cast<Options::Type>(options) != 7) { + cerr << "Cast to underlying type failed" << endl; + return TestFail; + } + + /* + * Conversion of the result of ninary operators on the underlying enum. + */ + + /* unsigned int val1 = Option::First; */ + /* unsigned int val2 = ~Option::First; */ + /* unsigned int val3 = Option::First | Option::Second; */ + /* Option val4 = ~Option::First; */ + /* Option val5 = Option::First | Option::Second; */ + + return TestPass; +} + +TEST_REGISTER(FlagsTest) diff --git a/test/geometry.cpp b/test/geometry.cpp index 27e65565..11df043b 100644 --- a/test/geometry.cpp +++ b/test/geometry.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * geometry.cpp - Geometry classes tests + * Geometry classes tests */ #include <iostream> @@ -17,16 +17,15 @@ using namespace libcamera; class GeometryTest : public Test { protected: - bool compare(const Size &lhs, const Size &rhs, - bool (*op)(const Size &lhs, const Size &rhs), + template<typename T> + bool compare(const T &lhs, const T &rhs, + bool (*op)(const T &lhs, const T &rhs), const char *opName, bool expect) { bool result = op(lhs, rhs); if (result != expect) { - cout << "Size(" << lhs.width << ", " << lhs.height << ") " - << opName << " " - << "Size(" << rhs.width << ", " << rhs.height << ") " + cout << lhs << opName << " " << rhs << "test failed" << std::endl; return false; } @@ -36,6 +35,243 @@ protected: int run() { + /* + * Point tests + */ + + /* Equality */ + if (!compare(Point(50, 100), Point(50, 100), &operator==, "==", true)) + return TestFail; + + if (!compare(Point(-50, 100), Point(-50, 100), &operator==, "==", true)) + return TestFail; + + if (!compare(Point(50, -100), Point(50, -100), &operator==, "==", true)) + return TestFail; + + if (!compare(Point(-50, -100), Point(-50, -100), &operator==, "==", true)) + return TestFail; + + /* Inequality */ + if (!compare(Point(50, 100), Point(50, 100), &operator!=, "!=", false)) + return TestFail; + + if (!compare(Point(-50, 100), Point(-50, 100), &operator!=, "!=", false)) + return TestFail; + + if (!compare(Point(50, -100), Point(50, -100), &operator!=, "!=", false)) + return TestFail; + + if (!compare(Point(-50, -100), Point(-50, -100), &operator!=, "!=", false)) + return TestFail; + + if (!compare(Point(-50, 100), Point(50, 100), &operator!=, "!=", true)) + return TestFail; + + if (!compare(Point(50, -100), Point(50, 100), &operator!=, "!=", true)) + return TestFail; + + if (!compare(Point(-50, -100), Point(50, 100), &operator!=, "!=", true)) + return TestFail; + + /* Negation */ + if (Point(50, 100) != -Point(-50, -100) || + Point(50, 100) == -Point(50, -100) || + Point(50, 100) == -Point(-50, 100)) { + cout << "Point negation test failed" << endl; + return TestFail; + } + + /* Default constructor */ + if (Point() != Point(0, 0)) { + cout << "Default constructor test failed" << endl; + return TestFail; + } + + /* + * Size tests + */ + + if (!Size().isNull() || !Size(0, 0).isNull()) { + cout << "Null size incorrectly reported as not null" << endl; + return TestFail; + } + + if (Size(0, 100).isNull() || Size(100, 0).isNull() || Size(100, 100).isNull()) { + cout << "Non-null size incorrectly reported as null" << endl; + return TestFail; + } + + /* + * Test alignDownTo(), alignUpTo(), boundTo(), expandTo(), + * growBy() and shrinkBy() + */ + Size s(50, 50); + + s.alignDownTo(16, 16); + if (s != Size(48, 48)) { + cout << "Size::alignDownTo() test failed" << endl; + return TestFail; + } + + s.alignUpTo(32, 32); + if (s != Size(64, 64)) { + cout << "Size::alignUpTo() test failed" << endl; + return TestFail; + } + + s.boundTo({ 40, 40 }); + if (s != Size(40, 40)) { + cout << "Size::boundTo() test failed" << endl; + return TestFail; + } + + s.expandTo({ 50, 50 }); + if (s != Size(50, 50)) { + cout << "Size::expandTo() test failed" << endl; + return TestFail; + } + + s.growBy({ 10, 20 }); + if (s != Size(60, 70)) { + cout << "Size::growBy() test failed" << endl; + return TestFail; + } + + s.shrinkBy({ 20, 10 }); + if (s != Size(40, 60)) { + cout << "Size::shrinkBy() test failed" << endl; + return TestFail; + } + + s.shrinkBy({ 100, 100 }); + if (s != Size(0, 0)) { + cout << "Size::shrinkBy() clamp test failed" << endl; + return TestFail; + } + + s = Size(50,50).alignDownTo(16, 16).alignUpTo(32, 32) + .boundTo({ 40, 80 }).expandTo({ 16, 80 }) + .growBy({ 4, 4 }).shrinkBy({ 10, 20 }); + if (s != Size(34, 64)) { + cout << "Size chained in-place modifiers test failed" << endl; + return TestFail; + } + + /* + * Test alignedDownTo(), alignedUpTo(), boundedTo(), + * expandedTo(), grownBy() and shrunkBy() + */ + if (Size(0, 0).alignedDownTo(16, 8) != Size(0, 0) || + Size(1, 1).alignedDownTo(16, 8) != Size(0, 0) || + Size(16, 8).alignedDownTo(16, 8) != Size(16, 8)) { + cout << "Size::alignedDownTo() test failed" << endl; + return TestFail; + } + + if (Size(0, 0).alignedUpTo(16, 8) != Size(0, 0) || + Size(1, 1).alignedUpTo(16, 8) != Size(16, 8) || + Size(16, 8).alignedUpTo(16, 8) != Size(16, 8)) { + cout << "Size::alignedUpTo() test failed" << endl; + return TestFail; + } + + if (Size(0, 0).boundedTo({ 100, 100 }) != Size(0, 0) || + Size(200, 50).boundedTo({ 100, 100 }) != Size(100, 50) || + Size(50, 200).boundedTo({ 100, 100 }) != Size(50, 100)) { + cout << "Size::boundedTo() test failed" << endl; + return TestFail; + } + + if (Size(0, 0).expandedTo({ 100, 100 }) != Size(100, 100) || + Size(200, 50).expandedTo({ 100, 100 }) != Size(200, 100) || + Size(50, 200).expandedTo({ 100, 100 }) != Size(100, 200)) { + cout << "Size::expandedTo() test failed" << endl; + return TestFail; + } + + if (Size(0, 0).grownBy({ 10, 20 }) != Size(10, 20) || + Size(200, 50).grownBy({ 10, 20 }) != Size(210, 70)) { + cout << "Size::grownBy() test failed" << endl; + return TestFail; + } + + if (Size(200, 50).shrunkBy({ 10, 20 }) != Size(190, 30) || + Size(200, 50).shrunkBy({ 10, 100 }) != Size(190, 0) || + Size(200, 50).shrunkBy({ 300, 20 }) != Size(0, 30)) { + cout << "Size::shrunkBy() test failed" << endl; + return TestFail; + } + + /* Aspect ratio tests */ + if (Size(0, 0).boundedToAspectRatio(Size(4, 3)) != Size(0, 0) || + Size(1920, 1440).boundedToAspectRatio(Size(16, 9)) != Size(1920, 1080) || + Size(1920, 1440).boundedToAspectRatio(Size(65536, 36864)) != Size(1920, 1080) || + Size(1440, 1920).boundedToAspectRatio(Size(9, 16)) != Size(1080, 1920) || + Size(1920, 1080).boundedToAspectRatio(Size(4, 3)) != Size(1440, 1080) || + Size(1920, 1080).boundedToAspectRatio(Size(65536, 49152)) != Size(1440, 1080) || + Size(1024, 1024).boundedToAspectRatio(Size(1, 1)) != Size(1024, 1024) || + Size(1920, 1080).boundedToAspectRatio(Size(16, 9)) != Size(1920, 1080) || + Size(200, 100).boundedToAspectRatio(Size(16, 9)) != Size(177, 100) || + Size(300, 200).boundedToAspectRatio(Size(16, 9)) != Size(300, 168)) { + cout << "Size::boundedToAspectRatio() test failed" << endl; + return TestFail; + } + + if (Size(0, 0).expandedToAspectRatio(Size(4, 3)) != Size(0, 0) || + Size(1920, 1440).expandedToAspectRatio(Size(16, 9)) != Size(2560, 1440) || + Size(1920, 1440).expandedToAspectRatio(Size(65536, 36864)) != Size(2560, 1440) || + Size(1440, 1920).expandedToAspectRatio(Size(9, 16)) != Size(1440, 2560) || + Size(1920, 1080).expandedToAspectRatio(Size(4, 3)) != Size(1920, 1440) || + Size(1920, 1080).expandedToAspectRatio(Size(65536, 49152)) != Size(1920, 1440) || + Size(1024, 1024).expandedToAspectRatio(Size(1, 1)) != Size(1024, 1024) || + Size(1920, 1080).expandedToAspectRatio(Size(16, 9)) != Size(1920, 1080) || + Size(200, 100).expandedToAspectRatio(Size(16, 9)) != Size(200, 112) || + Size(300, 200).expandedToAspectRatio(Size(16, 9)) != Size(355, 200)) { + cout << "Size::expandedToAspectRatio() test failed" << endl; + return TestFail; + } + + /* Size::centeredTo() tests */ + if (Size(0, 0).centeredTo(Point(50, 100)) != Rectangle(50, 100, 0, 0) || + Size(0, 0).centeredTo(Point(-50, -100)) != Rectangle(-50, -100, 0, 0) || + Size(100, 200).centeredTo(Point(50, 100)) != Rectangle(0, 0, 100, 200) || + Size(100, 200).centeredTo(Point(-50, -100)) != Rectangle(-100, -200, 100, 200) || + Size(101, 201).centeredTo(Point(-50, -100)) != Rectangle(-100, -200, 101, 201) || + Size(101, 201).centeredTo(Point(-51, -101)) != Rectangle(-101, -201, 101, 201)) { + cout << "Size::centeredTo() test failed" << endl; + return TestFail; + } + + /* Scale a size by a float */ + if (Size(1000, 2000) * 2.0 != Size(2000, 4000) || + Size(300, 100) * 0.5 != Size(150, 50) || + Size(1, 2) * 1.6 != Size(1, 3)) { + cout << "Size::operator*() failed" << endl; + return TestFail; + } + + if (Size(1000, 2000) / 2.0 != Size(500, 1000) || + Size(300, 100) / 0.5 != Size(600, 200) || + Size(1000, 2000) / 3.0 != Size(333, 666)) { + cout << "Size::operator*() failed" << endl; + return TestFail; + } + + s = Size(300, 100); + s *= 0.3333; + if (s != Size(99, 33)) { + cout << "Size::operator*() test failed" << endl; + return TestFail; + } + + s = Size(300, 100); + s /= 3; + if (s != Size(100, 33)) { + cout << "Size::operator*() test failed" << endl; + return TestFail; + } + /* Test Size equality and inequality. */ if (!compare(Size(100, 100), Size(100, 100), &operator==, "==", true)) return TestFail; @@ -109,6 +345,167 @@ protected: if (!compare(Size(200, 100), Size(100, 200), &operator>=, ">=", true)) return TestFail; + /* + * Rectangle tests + */ + + /* Test Rectangle::isNull(). */ + if (!Rectangle(0, 0, 0, 0).isNull() || + !Rectangle(1, 1, 0, 0).isNull()) { + cout << "Null rectangle incorrectly reported as not null" << endl; + return TestFail; + } + + if (Rectangle(0, 0, 0, 1).isNull() || + Rectangle(0, 0, 1, 0).isNull() || + Rectangle(0, 0, 1, 1).isNull()) { + cout << "Non-null rectangle incorrectly reported as null" << endl; + return TestFail; + } + + /* Rectangle::size(), Rectangle::topLeft() and Rectangle::center() tests */ + if (Rectangle(-1, -2, 3, 4).size() != Size(3, 4) || + Rectangle(0, 0, 100000, 200000).size() != Size(100000, 200000)) { + cout << "Rectangle::size() test failed" << endl; + return TestFail; + } + + if (Rectangle(1, 2, 3, 4).topLeft() != Point(1, 2) || + Rectangle(-1, -2, 3, 4).topLeft() != Point(-1, -2)) { + cout << "Rectangle::topLeft() test failed" << endl; + return TestFail; + } + + if (Rectangle(0, 0, 300, 400).center() != Point(150, 200) || + Rectangle(-1000, -2000, 300, 400).center() != Point(-850, -1800) || + Rectangle(10, 20, 301, 401).center() != Point(160, 220) || + Rectangle(11, 21, 301, 401).center() != Point(161, 221) || + Rectangle(-1011, -2021, 301, 401).center() != Point(-861, -1821)) { + cout << "Rectangle::center() test failed" << endl; + return TestFail; + } + + /* Rectangle::boundedTo() (intersection function) */ + if (Rectangle(0, 0, 1000, 2000).boundedTo(Rectangle(0, 0, 1000, 2000)) != + Rectangle(0, 0, 1000, 2000) || + Rectangle(-500, -1000, 1000, 2000).boundedTo(Rectangle(0, 0, 1000, 2000)) != + Rectangle(0, 0, 500, 1000) || + Rectangle(500, 1000, 1000, 2000).boundedTo(Rectangle(0, 0, 1000, 2000)) != + Rectangle(500, 1000, 500, 1000) || + Rectangle(300, 400, 50, 100).boundedTo(Rectangle(0, 0, 1000, 2000)) != + Rectangle(300, 400, 50, 100) || + Rectangle(0, 0, 1000, 2000).boundedTo(Rectangle(300, 400, 50, 100)) != + Rectangle(300, 400, 50, 100) || + Rectangle(0, 0, 100, 100).boundedTo(Rectangle(50, 100, 100, 100)) != + Rectangle(50, 100, 50, 0) || + Rectangle(0, 0, 100, 100).boundedTo(Rectangle(100, 50, 100, 100)) != + Rectangle(100, 50, 0, 50) || + Rectangle(-10, -20, 10, 20).boundedTo(Rectangle(10, 20, 100, 100)) != + Rectangle(10, 20, 0, 0)) { + cout << "Rectangle::boundedTo() test failed" << endl; + return TestFail; + } + + /* Rectangle::enclosedIn() tests */ + if (Rectangle(10, 20, 300, 400).enclosedIn(Rectangle(-10, -20, 1300, 1400)) != + Rectangle(10, 20, 300, 400) || + Rectangle(-100, -200, 3000, 4000).enclosedIn(Rectangle(-10, -20, 1300, 1400)) != + Rectangle(-10, -20, 1300, 1400) || + Rectangle(-100, -200, 300, 400).enclosedIn(Rectangle(-10, -20, 1300, 1400)) != + Rectangle(-10, -20, 300, 400) || + Rectangle(5100, 6200, 300, 400).enclosedIn(Rectangle(-10, -20, 1300, 1400)) != + Rectangle(990, 980, 300, 400) || + Rectangle(100, -300, 150, 200).enclosedIn(Rectangle(50, 0, 200, 300)) != + Rectangle(100, 0, 150, 200) || + Rectangle(100, -300, 150, 1200).enclosedIn(Rectangle(50, 0, 200, 300)) != + Rectangle(100, 0, 150, 300) || + Rectangle(-300, 100, 200, 150).enclosedIn(Rectangle(0, 50, 300, 200)) != + Rectangle(0, 100, 200, 150) || + Rectangle(-300, 100, 1200, 150).enclosedIn(Rectangle(0, 50, 300, 200)) != + Rectangle(0, 100, 300, 150)) { + cout << "Rectangle::enclosedIn() test failed" << endl; + return TestFail; + } + + /* Rectange::scaledBy() tests */ + if (Rectangle(10, 20, 300, 400).scaledBy(Size(0, 0), Size(1, 1)) != + Rectangle(0, 0, 0, 0) || + Rectangle(10, -20, 300, 400).scaledBy(Size(32768, 65536), Size(32768, 32768)) != + Rectangle(10, -40, 300, 800) || + Rectangle(-30000, 10000, 20000, 20000).scaledBy(Size(7, 7), Size(7, 7)) != + Rectangle(-30000, 10000, 20000, 20000) || + Rectangle(-20, -30, 320, 240).scaledBy(Size(1280, 960), Size(640, 480)) != + Rectangle(-40, -60, 640, 480) || + Rectangle(1, 1, 2026, 1510).scaledBy(Size(4056, 3024), Size(2028, 1512)) != + Rectangle(2, 2, 4052, 3020)) { + cout << "Rectangle::scaledBy() test failed" << endl; + return TestFail; + } + + /* Rectangle::translatedBy() tests */ + if (Rectangle(10, -20, 300, 400).translatedBy(Point(-30, 40)) != + Rectangle(-20, 20, 300, 400) || + Rectangle(-10, 20, 400, 300).translatedBy(Point(50, -60)) != + Rectangle(40, -40, 400, 300)) { + cout << "Rectangle::translatedBy() test failed" << endl; + return TestFail; + } + + /* Rectangle::scaleBy() tests */ + Rectangle r(-20, -30, 320, 240); + r.scaleBy(Size(1280, 960), Size(640, 480)); + if (r != Rectangle(-40, -60, 640, 480)) { + cout << "Rectangle::scaleBy() test failed" << endl; + return TestFail; + } + + r = Rectangle(1, 1, 2026, 1510); + r.scaleBy(Size(4056, 3024), Size(2028, 1512)); + if (r != Rectangle(2, 2, 4052, 3020)) { + cout << "Rectangle::scaleBy() test failed" << endl; + return TestFail; + } + + /* Rectangle::translateBy() tests */ + r = Rectangle(10, -20, 300, 400); + r.translateBy(Point(-30, 40)); + if (r != Rectangle(-20, 20, 300, 400)) { + cout << "Rectangle::translateBy() test failed" << endl; + return TestFail; + } + + r = Rectangle(-10, 20, 400, 300); + r.translateBy(Point(50, -60)); + if (r != Rectangle(40, -40, 400, 300)) { + cout << "Rectangle::translateBy() test failed" << endl; + return TestFail; + } + + Point topLeft(3, 3); + Point bottomRight(30, 30); + Point topRight(30, 3); + Point bottomLeft(3, 30); + Rectangle rect1(topLeft, bottomRight); + Rectangle rect2(topRight, bottomLeft); + Rectangle rect3(bottomRight, topLeft); + Rectangle rect4(bottomLeft, topRight); + + if (rect1 != rect2 || rect1 != rect3 || rect1 != rect4) { + cout << "Point-to-point construction failed" << endl; + return TestFail; + } + + Rectangle f1 = Rectangle(100, 200, 3000, 2000); + Rectangle f2 = Rectangle(200, 300, 1500, 1000); + /* Bottom right quarter of the corresponding frames. */ + Rectangle r1 = Rectangle(100 + 1500, 200 + 1000, 1500, 1000); + Rectangle r2 = Rectangle(200 + 750, 300 + 500, 750, 500); + if (r1.transformedBetween(f1, f2) != r2 || + r2.transformedBetween(f2, f1) != r1) { + cout << "Rectangle::transformedBetween() test failed" << endl; + return TestFail; + } + return TestPass; } }; diff --git a/test/gstreamer/gstreamer_device_provider_test.cpp b/test/gstreamer/gstreamer_device_provider_test.cpp new file mode 100644 index 00000000..521c60b8 --- /dev/null +++ b/test/gstreamer/gstreamer_device_provider_test.cpp @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2023, Umang Jain <umang.jain@ideasonboard.com> + * + * GStreamer single stream capture test + */ + +#include <vector> + +#include <libcamera/libcamera.h> +#include <gst/gst.h> + +#include "gstreamer_test.h" +#include "test.h" + +using namespace std; + +class GstreamerDeviceProviderTest : public GstreamerTest, public Test +{ +public: + GstreamerDeviceProviderTest() + : GstreamerTest() + { + } + +protected: + int init() override + { + if (status_ != TestPass) + return status_; + + return TestPass; + } + + int run() override + { + g_autoptr(GstDeviceProvider) provider = NULL; + GList *devices, *l; + std::vector<std::string> cameraNames; + std::unique_ptr<libcamera::CameraManager> cm; + + cm = std::make_unique<libcamera::CameraManager>(); + cm->start(); + for (auto &camera : cm->cameras()) + cameraNames.push_back(camera->id()); + cm->stop(); + cm.reset(); + + provider = gst_device_provider_factory_get_by_name("libcameraprovider"); + devices = gst_device_provider_get_devices(provider); + + for (l = devices; l != NULL; l = g_list_next(l)) { + GstDevice *device = GST_DEVICE(l->data); + g_autofree gchar *gst_name; + + g_autoptr(GstElement) element = gst_device_create_element(device, NULL); + g_object_get(element, "camera-name", &gst_name, NULL); + + if (std::find(cameraNames.begin(), cameraNames.end(), gst_name) == + cameraNames.end()) + return TestFail; + } + + g_list_free_full(devices, (GDestroyNotify)gst_object_unref); + + return TestPass; + } +}; + +TEST_REGISTER(GstreamerDeviceProviderTest) diff --git a/test/gstreamer/gstreamer_memory_lifetime_test.cpp b/test/gstreamer/gstreamer_memory_lifetime_test.cpp new file mode 100644 index 00000000..1738cf56 --- /dev/null +++ b/test/gstreamer/gstreamer_memory_lifetime_test.cpp @@ -0,0 +1,90 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2024, Nicolas Dufresne + * + * gstreamer_memory_lifetime_test.cpp - GStreamer memory lifetime test + */ + +#include <iostream> +#include <unistd.h> + +#include <gst/app/app.h> +#include <gst/gst.h> + +#include "gstreamer_test.h" +#include "test.h" + +using namespace std; + +class GstreamerMemoryLifetimeTest : public GstreamerTest, public Test +{ +public: + GstreamerMemoryLifetimeTest() + : GstreamerTest() + { + } + +protected: + int init() override + { + if (status_ != TestPass) + return status_; + + appsink_ = gst_element_factory_make("appsink", nullptr); + if (!appsink_) { + g_printerr("Your installation is missing 'appsink'\n"); + return TestFail; + } + g_object_ref_sink(appsink_); + + return createPipeline(); + } + + int run() override + { + /* Build the pipeline */ + gst_bin_add_many(GST_BIN(pipeline_), libcameraSrc_, appsink_, nullptr); + if (gst_element_link(libcameraSrc_, appsink_) != TRUE) { + g_printerr("Elements could not be linked.\n"); + return TestFail; + } + + if (startPipeline() != TestPass) + return TestFail; + + sample_ = gst_app_sink_try_pull_sample(GST_APP_SINK(appsink_), GST_SECOND * 5); + if (!sample_) { + /* Failed to obtain a sample. Abort the test */ + gst_element_set_state(pipeline_, GST_STATE_NULL); + return TestFail; + } + + /* + * Keep the sample referenced and set the pipeline state to + * NULL. This causes the libcamerasrc element to synchronously + * release resources it holds. The sample will be released + * later in cleanup(). + * + * The test case verifies that libcamerasrc keeps alive long + * enough all the resources that are needed until memory + * allocated for frames gets freed. We return TestPass at this + * stage, and any use-after-free will be caught by the test + * crashing in cleanup(). + */ + gst_element_set_state(pipeline_, GST_STATE_NULL); + + return TestPass; + } + + void cleanup() override + { + g_clear_pointer(&sample_, gst_sample_unref); + g_clear_object(&appsink_); + } + +private: + GstElement *appsink_; + GstSample *sample_; +}; + +TEST_REGISTER(GstreamerMemoryLifetimeTest) diff --git a/test/gstreamer/gstreamer_multi_stream_test.cpp b/test/gstreamer/gstreamer_multi_stream_test.cpp new file mode 100644 index 00000000..263d1e86 --- /dev/null +++ b/test/gstreamer/gstreamer_multi_stream_test.cpp @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Vedant Paranjape + * + * GStreamer multi stream capture test + */ + +#include <iostream> +#include <unistd.h> + +#include <libcamera/libcamera.h> + +#include <gst/gst.h> + +#include "gstreamer_test.h" +#include "test.h" + +#if !GST_CHECK_VERSION(1, 19, 1) +static inline GstPad *gst_element_request_pad_simple(GstElement *element, + const gchar *name) +{ + return gst_element_get_request_pad(element, name); +} +#endif + +using namespace std; + +class GstreamerMultiStreamTest : public GstreamerTest, public Test +{ +public: + GstreamerMultiStreamTest() + : GstreamerTest(2) + { + } + +protected: + int init() override + { + if (status_ != TestPass) + return status_; + + const gchar *streamDescription = "queue ! fakesink"; + g_autoptr(GError) error = NULL; + + stream0_ = gst_parse_bin_from_description_full(streamDescription, TRUE, + NULL, + GST_PARSE_FLAG_FATAL_ERRORS, + &error); + if (!stream0_) { + g_printerr("Stream0 could not be created (%s)\n", error->message); + return TestFail; + } + g_object_ref_sink(stream0_); + + stream1_ = gst_parse_bin_from_description_full(streamDescription, TRUE, + NULL, + GST_PARSE_FLAG_FATAL_ERRORS, + &error); + if (!stream1_) { + g_printerr("Stream1 could not be created (%s)\n", error->message); + return TestFail; + } + g_object_ref_sink(stream1_); + + if (createPipeline() != TestPass) + return TestFail; + + return TestPass; + } + + int run() override + { + /* Build the pipeline */ + gst_bin_add_many(GST_BIN(pipeline_), libcameraSrc_, + stream0_, stream1_, NULL); + + g_autoptr(GstPad) src_pad = gst_element_get_static_pad(libcameraSrc_, "src"); + g_autoptr(GstPad) request_pad = gst_element_request_pad_simple(libcameraSrc_, "src_%u"); + + { + g_autoptr(GstPad) queue0_sink_pad = gst_element_get_static_pad(stream0_, "sink"); + g_autoptr(GstPad) queue1_sink_pad = gst_element_get_static_pad(stream1_, "sink"); + + if (gst_pad_link(src_pad, queue0_sink_pad) != GST_PAD_LINK_OK || + gst_pad_link(request_pad, queue1_sink_pad) != GST_PAD_LINK_OK) { + g_printerr("Pads could not be linked.\n"); + return TestFail; + } + } + + if (startPipeline() != TestPass) + return TestFail; + + if (processEvent() != TestPass) + return TestFail; + + return TestPass; + } + + void cleanup() override + { + g_clear_object(&stream0_); + g_clear_object(&stream1_); + } + +private: + GstElement *stream0_; + GstElement *stream1_; +}; + +TEST_REGISTER(GstreamerMultiStreamTest) diff --git a/test/gstreamer/gstreamer_single_stream_test.cpp b/test/gstreamer/gstreamer_single_stream_test.cpp new file mode 100644 index 00000000..3ef2d323 --- /dev/null +++ b/test/gstreamer/gstreamer_single_stream_test.cpp @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Vedant Paranjape + * + * GStreamer single stream capture test + */ + +#include <iostream> +#include <unistd.h> + +#include <gst/gst.h> + +#include "gstreamer_test.h" +#include "test.h" + +using namespace std; + +class GstreamerSingleStreamTest : public GstreamerTest, public Test +{ +public: + GstreamerSingleStreamTest() + : GstreamerTest() + { + } + +protected: + int init() override + { + if (status_ != TestPass) + return status_; + + fakesink_ = gst_element_factory_make("fakesink", nullptr); + if (!fakesink_) { + g_printerr("Your installation is missing 'fakesink'\n"); + return TestFail; + } + g_object_ref_sink(fakesink_); + + return createPipeline(); + } + + int run() override + { + /* Build the pipeline */ + gst_bin_add_many(GST_BIN(pipeline_), libcameraSrc_, fakesink_, nullptr); + if (!gst_element_link(libcameraSrc_, fakesink_)) { + g_printerr("Elements could not be linked.\n"); + return TestFail; + } + + if (startPipeline() != TestPass) + return TestFail; + + if (processEvent() != TestPass) + return TestFail; + + return TestPass; + } + + void cleanup() override + { + g_clear_object(&fakesink_); + } + +private: + GstElement *fakesink_; +}; + +TEST_REGISTER(GstreamerSingleStreamTest) diff --git a/test/gstreamer/gstreamer_test.cpp b/test/gstreamer/gstreamer_test.cpp new file mode 100644 index 00000000..a15fef0e --- /dev/null +++ b/test/gstreamer/gstreamer_test.cpp @@ -0,0 +1,174 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Vedant Paranjape + * + * libcamera Gstreamer element API tests + */ + +#include <libcamera/libcamera.h> + +#include <libcamera/base/utils.h> + +#if HAVE_ASAN +#include <sanitizer/asan_interface.h> +#endif + +#include "gstreamer_test.h" + +#include "test.h" + +using namespace std; + +#if HAVE_ASAN +extern "C" { +const char *__asan_default_options() +{ + /* + * Disable leak detection due to a known global variable initialization + * leak in glib's g_quark_init(). This should ideally be handled by + * using a suppression file instead of disabling leak detection. + */ + return "detect_leaks=false"; +} +} +#endif + +GstreamerTest::GstreamerTest(unsigned int numStreams) + : pipeline_(nullptr), libcameraSrc_(nullptr) +{ + /* + * GStreamer by default spawns a process to run the gst-plugin-scanner + * helper. If libcamera is compiled with ASan enabled, and as GStreamer + * is most likely not, this causes the ASan link order check to fail + * when gst-plugin-scanner dlopen()s the plugin as many libraries will + * have already been loaded by then. Fix this issue by disabling + * spawning of a child helper process when scanning the build directory + * for plugins. + */ + gst_registry_fork_set_enabled(false); + + /* Initialize GStreamer */ + g_autoptr(GError) errInit = NULL; + if (!gst_init_check(nullptr, nullptr, &errInit)) { + g_printerr("Could not initialize GStreamer: %s\n", + errInit ? errInit->message : "unknown error"); + + status_ = TestFail; + return; + } + + /* + * Atleast one camera should be available with numStreams streams, + * otherwise skip the test entirely. + */ + if (!checkMinCameraStreamsAndSetCameraName(numStreams)) { + status_ = TestSkip; + return; + } + + status_ = TestPass; +} + +bool GstreamerTest::checkMinCameraStreamsAndSetCameraName(unsigned int numStreams) +{ + libcamera::CameraManager cm; + bool cameraFound = false; + + cm.start(); + + for (auto &camera : cm.cameras()) { + if (camera->streams().size() < numStreams) + continue; + + cameraFound = true; + cameraName_ = camera->id(); + break; + } + + cm.stop(); + + return cameraFound; +} + +GstreamerTest::~GstreamerTest() +{ + g_clear_object(&pipeline_); + g_clear_object(&libcameraSrc_); + + gst_deinit(); +} + +int GstreamerTest::createPipeline() +{ + libcameraSrc_ = gst_element_factory_make("libcamerasrc", "libcamera"); + pipeline_ = gst_pipeline_new("test-pipeline"); + + if (!libcameraSrc_ || !pipeline_) { + g_printerr("Unable to create pipeline %p.%p\n", + libcameraSrc_, pipeline_); + + return TestFail; + } + + g_object_set(libcameraSrc_, "camera-name", cameraName_.c_str(), NULL); + g_object_ref_sink(libcameraSrc_); + + return TestPass; +} + +int GstreamerTest::startPipeline() +{ + GstStateChangeReturn ret; + + /* Start playing */ + ret = gst_element_set_state(pipeline_, GST_STATE_PLAYING); + if (ret == GST_STATE_CHANGE_FAILURE) { + g_printerr("Unable to set the pipeline to the playing state.\n"); + return TestFail; + } + + return TestPass; +} + +int GstreamerTest::processEvent() +{ + /* Wait until error or EOS or timeout after 2 seconds */ + constexpr GstMessageType msgType = + static_cast<GstMessageType>(GST_MESSAGE_ERROR | GST_MESSAGE_EOS); + constexpr GstClockTime timeout = 2 * GST_SECOND; + + g_autoptr(GstBus) bus = gst_element_get_bus(pipeline_); + g_autoptr(GstMessage) msg = gst_bus_timed_pop_filtered(bus, timeout, msgType); + + gst_element_set_state(pipeline_, GST_STATE_NULL); + + /* Parse error message */ + if (msg == NULL) + return TestPass; + + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_ERROR: + printError(msg); + break; + case GST_MESSAGE_EOS: + g_print("End-Of-Stream reached.\n"); + break; + default: + g_printerr("Unexpected message received.\n"); + break; + } + + return TestFail; +} + +void GstreamerTest::printError(GstMessage *msg) +{ + g_autoptr(GError) err = NULL; + g_autofree gchar *debug_info = NULL; + + gst_message_parse_error(msg, &err, &debug_info); + g_printerr("Error received from element %s: %s\n", + GST_OBJECT_NAME(msg->src), err->message); + g_printerr("Debugging information: %s\n", + debug_info ? debug_info : "none"); +} diff --git a/test/gstreamer/gstreamer_test.h b/test/gstreamer/gstreamer_test.h new file mode 100644 index 00000000..abb37c1b --- /dev/null +++ b/test/gstreamer/gstreamer_test.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Vedant Paranjape + * + * GStreamer test base class + */ + +#pragma once + +#include <iostream> +#include <unistd.h> + +#include <gst/gst.h> + +class GstreamerTest +{ +public: + GstreamerTest(unsigned int numStreams = 1); + virtual ~GstreamerTest(); + +protected: + virtual int createPipeline(); + int startPipeline(); + int processEvent(); + void printError(GstMessage *msg); + + std::string cameraName_; + GstElement *pipeline_; + GstElement *libcameraSrc_; + int status_; + +private: + bool checkMinCameraStreamsAndSetCameraName(unsigned int numStreams); +}; diff --git a/test/gstreamer/meson.build b/test/gstreamer/meson.build new file mode 100644 index 00000000..e066c582 --- /dev/null +++ b/test/gstreamer/meson.build @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: CC0-1.0 + +if not gst_enabled + subdir_done() +endif + +gstreamer_tests = [ + {'name': 'single_stream_test', 'sources': ['gstreamer_single_stream_test.cpp']}, + {'name': 'multi_stream_test', 'sources': ['gstreamer_multi_stream_test.cpp']}, + {'name': 'device_provider_test', 'sources': ['gstreamer_device_provider_test.cpp']}, + {'name': 'memory_lifetime_test', 'sources': ['gstreamer_memory_lifetime_test.cpp']}, +] +gstreamer_dep = dependency('gstreamer-1.0', required : true) +gstapp_dep = dependency('gstreamer-app-1.0', required : true) + +gstreamer_test_args = [] + +if asan_enabled + gstreamer_test_args += ['-D', 'HAVE_ASAN=1'] +endif + +foreach test : gstreamer_tests + exe = executable(test['name'], test['sources'], 'gstreamer_test.cpp', + cpp_args : gstreamer_test_args, + dependencies : [libcamera_private, gstreamer_dep, gstapp_dep], + link_with : test_libraries, + include_directories : test_includes_internal) + + test(test['name'], exe, suite : 'gstreamer', is_parallel : false, + env : gst_env, should_fail : test.get('should_fail', false)) +endforeach diff --git a/test/hotplug-cameras.cpp b/test/hotplug-cameras.cpp new file mode 100644 index 00000000..530e9a31 --- /dev/null +++ b/test/hotplug-cameras.cpp @@ -0,0 +1,129 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Umang Jain <email@uajain.com> + * + * Test cameraAdded/cameraRemoved signals in CameraManager + */ + +#include <dirent.h> +#include <fstream> +#include <iostream> +#include <string.h> +#include <unistd.h> + +#include <libcamera/camera.h> +#include <libcamera/camera_manager.h> + +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/file.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> + +#include "test.h" + +using namespace libcamera; +using namespace std::chrono_literals; + +class HotplugTest : public Test +{ +protected: + void cameraAddedHandler([[maybe_unused]] std::shared_ptr<Camera> cam) + { + cameraAdded_ = true; + } + + void cameraRemovedHandler([[maybe_unused]] std::shared_ptr<Camera> cam) + { + cameraRemoved_ = true; + } + + int init() + { + if (!File::exists("/sys/module/uvcvideo")) { + std::cout << "uvcvideo driver is not loaded, skipping" << std::endl; + return TestSkip; + } + + if (geteuid() != 0) { + std::cout << "This test requires root permissions, skipping" << std::endl; + return TestSkip; + } + + cm_ = new CameraManager(); + if (cm_->start()) { + std::cout << "Failed to start camera manager" << std::endl; + return TestFail; + } + + cameraAdded_ = false; + cameraRemoved_ = false; + + cm_->cameraAdded.connect(this, &HotplugTest::cameraAddedHandler); + cm_->cameraRemoved.connect(this, &HotplugTest::cameraRemovedHandler); + + return 0; + } + + int run() + { + DIR *dir; + struct dirent *dirent; + std::string uvcDeviceDir; + + dir = opendir(uvcDriverDir_.c_str()); + /* Find a UVC device directory, which we can bind/unbind. */ + while ((dirent = readdir(dir)) != nullptr) { + if (!File::exists(uvcDriverDir_ + dirent->d_name + "/video4linux")) + continue; + + uvcDeviceDir = dirent->d_name; + break; + } + closedir(dir); + + /* If no UVC device found, skip the test. */ + if (uvcDeviceDir.empty()) + return TestSkip; + + /* Unbind a camera and process events. */ + std::ofstream(uvcDriverDir_ + "unbind", std::ios::binary) + << uvcDeviceDir; + Timer timer; + timer.start(1000ms); + while (timer.isRunning() && !cameraRemoved_) + Thread::current()->eventDispatcher()->processEvents(); + if (!cameraRemoved_) { + std::cout << "Camera unplug not detected" << std::endl; + return TestFail; + } + + /* Bind the camera again and process events. */ + std::ofstream(uvcDriverDir_ + "bind", std::ios::binary) + << uvcDeviceDir; + timer.start(1000ms); + while (timer.isRunning() && !cameraAdded_) + Thread::current()->eventDispatcher()->processEvents(); + if (!cameraAdded_) { + std::cout << "Camera plug not detected" << std::endl; + return TestFail; + } + + return TestPass; + } + + void cleanup() + { + cm_->stop(); + delete cm_; + } + +private: + CameraManager *cm_; + static const std::string uvcDriverDir_; + bool cameraRemoved_; + bool cameraAdded_; +}; + +const std::string HotplugTest::uvcDriverDir_ = "/sys/bus/usb/drivers/uvcvideo/"; + +TEST_REGISTER(HotplugTest) diff --git a/test/ipa/ipa_interface_test.cpp b/test/ipa/ipa_interface_test.cpp index cafc249b..b8178366 100644 --- a/test/ipa/ipa_interface_test.cpp +++ b/test/ipa/ipa_interface_test.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * ipa_interface_test.cpp - Test the IPA interface + * Test the IPA interface */ #include <fcntl.h> @@ -12,44 +12,52 @@ #include <sys/types.h> #include <unistd.h> -#include <libcamera/event_dispatcher.h> -#include <libcamera/event_notifier.h> -#include <libcamera/timer.h> +#include <libcamera/ipa/vimc_ipa_proxy.h> -#include <ipa/ipa_vimc.h> +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/event_notifier.h> +#include <libcamera/base/object.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> + +#include "libcamera/internal/camera_manager.h" +#include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/ipa_manager.h" +#include "libcamera/internal/ipa_module.h" +#include "libcamera/internal/pipeline_handler.h" -#include "device_enumerator.h" -#include "ipa_manager.h" -#include "ipa_module.h" -#include "pipeline_handler.h" #include "test.h" -#include "thread.h" -using namespace std; using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; class IPAInterfaceTest : public Test, public Object { public: IPAInterfaceTest() - : trace_(IPAOperationNone), notifier_(nullptr), fd_(-1) + : trace_(ipa::vimc::IPAOperationNone), notifier_(nullptr), fd_(-1) { } ~IPAInterfaceTest() { delete notifier_; + ipa_.reset(); + cameraManager_.reset(); } protected: int init() override { + cameraManager_ = make_unique<CameraManager>(); + /* Create a pipeline handler for vimc. */ - std::vector<PipelineHandlerFactory *> &factories = - PipelineHandlerFactory::factories(); - for (PipelineHandlerFactory *factory : factories) { - if (factory->name() == "PipelineHandlerVimc") { - pipe_ = factory->create(nullptr); + const std::vector<PipelineHandlerFactoryBase *> &factories = + PipelineHandlerFactoryBase::factories(); + for (const PipelineHandlerFactoryBase *factory : factories) { + if (factory->name() == "vimc") { + pipe_ = factory->create(cameraManager_.get()); break; } } @@ -60,22 +68,22 @@ protected: } /* Create and open the communication FIFO. */ - int ret = mkfifo(VIMC_IPA_FIFO_PATH, S_IRUSR | S_IWUSR); + int ret = mkfifo(ipa::vimc::VimcIPAFIFOPath.c_str(), S_IRUSR | S_IWUSR); if (ret) { ret = errno; cerr << "Failed to create IPA test FIFO at '" - << VIMC_IPA_FIFO_PATH << "': " << strerror(ret) + << ipa::vimc::VimcIPAFIFOPath << "': " << strerror(ret) << endl; return TestFail; } - ret = open(VIMC_IPA_FIFO_PATH, O_RDONLY | O_NONBLOCK); + ret = open(ipa::vimc::VimcIPAFIFOPath.c_str(), O_RDONLY | O_NONBLOCK); if (ret < 0) { ret = errno; cerr << "Failed to open IPA test FIFO at '" - << VIMC_IPA_FIFO_PATH << "': " << strerror(ret) + << ipa::vimc::VimcIPAFIFOPath << "': " << strerror(ret) << endl; - unlink(VIMC_IPA_FIFO_PATH); + unlink(ipa::vimc::VimcIPAFIFOPath.c_str()); return TestFail; } fd_ = ret; @@ -91,49 +99,82 @@ protected: EventDispatcher *dispatcher = thread()->eventDispatcher(); Timer timer; - ipa_ = IPAManager::instance()->createIPA(pipe_.get(), 0, 0); + ipa_ = IPAManager::createIPA<ipa::vimc::IPAProxyVimc>(pipe_.get(), 0, 0); if (!ipa_) { cerr << "Failed to create VIMC IPA interface" << endl; return TestFail; } /* Test initialization of IPA module. */ - ipa_->init(); - timer.start(1000); - while (timer.isRunning() && trace_ != IPAOperationInit) + std::string conf = ipa_->configurationFile("vimc.conf"); + Flags<ipa::vimc::TestFlag> inFlags; + Flags<ipa::vimc::TestFlag> outFlags; + int ret = ipa_->init(IPASettings{ conf, "vimc" }, + ipa::vimc::IPAOperationInit, + inFlags, &outFlags); + if (ret < 0) { + cerr << "IPA interface init() failed" << endl; + return TestFail; + } + + timer.start(1000ms); + while (timer.isRunning() && trace_ != ipa::vimc::IPAOperationInit) dispatcher->processEvents(); - if (trace_ != IPAOperationInit) { + if (trace_ != ipa::vimc::IPAOperationInit) { cerr << "Failed to test IPA initialization sequence" << endl; return TestFail; } + /* Test start of IPA module. */ + ipa_->start(); + timer.start(1000ms); + while (timer.isRunning() && trace_ != ipa::vimc::IPAOperationStart) + dispatcher->processEvents(); + + if (trace_ != ipa::vimc::IPAOperationStart) { + cerr << "Failed to test IPA start sequence" << endl; + return TestFail; + } + + /* Test stop of IPA module. */ + ipa_->stop(); + timer.start(1000ms); + while (timer.isRunning() && trace_ != ipa::vimc::IPAOperationStop) + dispatcher->processEvents(); + + if (trace_ != ipa::vimc::IPAOperationStop) { + cerr << "Failed to test IPA stop sequence" << endl; + return TestFail; + } + return TestPass; } void cleanup() override { close(fd_); - unlink(VIMC_IPA_FIFO_PATH); + unlink(ipa::vimc::VimcIPAFIFOPath.c_str()); } private: - void readTrace(EventNotifier *notifier) + void readTrace() { - ssize_t s = read(notifier->fd(), &trace_, sizeof(trace_)); + ssize_t s = read(notifier_->fd(), &trace_, sizeof(trace_)); if (s < 0) { int ret = errno; cerr << "Failed to read from IPA test FIFO at '" - << VIMC_IPA_FIFO_PATH << "': " << strerror(ret) + << ipa::vimc::VimcIPAFIFOPath << "': " << strerror(ret) << endl; - trace_ = IPAOperationNone; + trace_ = ipa::vimc::IPAOperationNone; } } std::shared_ptr<PipelineHandler> pipe_; - std::unique_ptr<IPAInterface> ipa_; - enum IPAOperationCode trace_; + std::unique_ptr<ipa::vimc::IPAProxyVimc> ipa_; + std::unique_ptr<CameraManager> cameraManager_; + enum ipa::vimc::IPAOperationCode trace_; EventNotifier *notifier_; int fd_; }; diff --git a/test/ipa/ipa_module_test.cpp b/test/ipa/ipa_module_test.cpp index 287e53fd..1c97da32 100644 --- a/test/ipa/ipa_module_test.cpp +++ b/test/ipa/ipa_module_test.cpp @@ -2,13 +2,13 @@ /* * Copyright (C) 2019, Google Inc. * - * ipa_module_test.cpp - Test loading of the VIMC IPA module and verify its info + * Test loading of the VIMC IPA module and verify its info */ #include <iostream> #include <string.h> -#include "ipa_module.h" +#include "libcamera/internal/ipa_module.h" #include "test.h" @@ -57,9 +57,8 @@ protected: const struct IPAModuleInfo testInfo = { IPA_MODULE_API_VERSION, 0, - "PipelineHandlerVimc", - "Dummy IPA for Vimc", - "GPL-2.0-or-later", + "vimc", + "vimc", }; count += runTest("src/ipa/vimc/ipa_vimc.so", testInfo); diff --git a/test/ipa/ipa_wrappers_test.cpp b/test/ipa/ipa_wrappers_test.cpp deleted file mode 100644 index 1ae17811..00000000 --- a/test/ipa/ipa_wrappers_test.cpp +++ /dev/null @@ -1,394 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* - * Copyright (C) 2019, Google Inc. - * - * ipa_wrappers_test.cpp - Test the IPA interface and context wrappers - */ - -#include <fcntl.h> -#include <iostream> -#include <memory> -#include <linux/videodev2.h> -#include <sys/stat.h> -#include <unistd.h> - -#include <libcamera/controls.h> -#include <libipa/ipa_interface_wrapper.h> - -#include "device_enumerator.h" -#include "ipa_context_wrapper.h" -#include "media_device.h" -#include "v4l2_subdevice.h" - -#include "test.h" - -using namespace libcamera; -using namespace std; - -enum Operation { - Op_init, - Op_configure, - Op_mapBuffers, - Op_unmapBuffers, - Op_processEvent, -}; - -class TestIPAInterface : public IPAInterface -{ -public: - TestIPAInterface() - : sequence_(0) - { - } - - int init() override - { - report(Op_init, TestPass); - return 0; - } - - void configure(const std::map<unsigned int, IPAStream> &streamConfig, - const std::map<unsigned int, const ControlInfoMap &> &entityControls) override - { - /* Verify streamConfig. */ - if (streamConfig.size() != 2) { - cerr << "configure(): Invalid number of streams " - << streamConfig.size() << endl; - return report(Op_configure, TestFail); - } - - auto iter = streamConfig.find(1); - if (iter == streamConfig.end()) { - cerr << "configure(): No configuration for stream 1" << endl; - return report(Op_configure, TestFail); - } - const IPAStream *stream = &iter->second; - if (stream->pixelFormat != V4L2_PIX_FMT_YUYV || - stream->size != Size{ 1024, 768 }) { - cerr << "configure(): Invalid configuration for stream 1" << endl; - return report(Op_configure, TestFail); - } - - iter = streamConfig.find(2); - if (iter == streamConfig.end()) { - cerr << "configure(): No configuration for stream 2" << endl; - return report(Op_configure, TestFail); - } - stream = &iter->second; - if (stream->pixelFormat != V4L2_PIX_FMT_NV12 || - stream->size != Size{ 800, 600 }) { - cerr << "configure(): Invalid configuration for stream 2" << endl; - return report(Op_configure, TestFail); - } - - /* Verify entityControls. */ - auto ctrlIter = entityControls.find(42); - if (ctrlIter == entityControls.end()) { - cerr << "configure(): Controls not found" << endl; - return report(Op_configure, TestFail); - } - - const ControlInfoMap &infoMap = ctrlIter->second; - - if (infoMap.count(V4L2_CID_BRIGHTNESS) != 1 || - infoMap.count(V4L2_CID_CONTRAST) != 1 || - infoMap.count(V4L2_CID_SATURATION) != 1) { - cerr << "configure(): Invalid control IDs" << endl; - return report(Op_configure, TestFail); - } - - report(Op_configure, TestPass); - } - - void mapBuffers(const std::vector<IPABuffer> &buffers) override - { - if (buffers.size() != 2) { - cerr << "mapBuffers(): Invalid number of buffers " - << buffers.size() << endl; - return report(Op_mapBuffers, TestFail); - } - - if (buffers[0].id != 10 || - buffers[1].id != 11) { - cerr << "mapBuffers(): Invalid buffer IDs" << endl; - return report(Op_mapBuffers, TestFail); - } - - if (buffers[0].planes.size() != 3 || - buffers[1].planes.size() != 3) { - cerr << "mapBuffers(): Invalid number of planes" << endl; - return report(Op_mapBuffers, TestFail); - } - - if (buffers[0].planes[0].length != 4096 || - buffers[0].planes[1].length != 0 || - buffers[0].planes[2].length != 0 || - buffers[0].planes[0].length != 4096 || - buffers[1].planes[1].length != 4096 || - buffers[1].planes[2].length != 0) { - cerr << "mapBuffers(): Invalid length" << endl; - return report(Op_mapBuffers, TestFail); - } - - if (buffers[0].planes[0].fd.fd() == -1 || - buffers[0].planes[1].fd.fd() != -1 || - buffers[0].planes[2].fd.fd() != -1 || - buffers[0].planes[0].fd.fd() == -1 || - buffers[1].planes[1].fd.fd() == -1 || - buffers[1].planes[2].fd.fd() != -1) { - cerr << "mapBuffers(): Invalid dmabuf" << endl; - return report(Op_mapBuffers, TestFail); - } - - report(Op_mapBuffers, TestPass); - } - - void unmapBuffers(const std::vector<unsigned int> &ids) override - { - if (ids.size() != 2) { - cerr << "unmapBuffers(): Invalid number of ids " - << ids.size() << endl; - return report(Op_unmapBuffers, TestFail); - } - - if (ids[0] != 10 || ids[1] != 11) { - cerr << "unmapBuffers(): Invalid buffer IDs" << endl; - return report(Op_unmapBuffers, TestFail); - } - - report(Op_unmapBuffers, TestPass); - } - - void processEvent(const IPAOperationData &data) override - { - /* Verify operation and data. */ - if (data.operation != Op_processEvent) { - cerr << "processEvent(): Invalid operation " - << data.operation << endl; - return report(Op_processEvent, TestFail); - } - - if (data.data != std::vector<unsigned int>{ 1, 2, 3, 4 }) { - cerr << "processEvent(): Invalid data" << endl; - return report(Op_processEvent, TestFail); - } - - /* Verify controls. */ - if (data.controls.size() != 1) { - cerr << "processEvent(): Controls not found" << endl; - return report(Op_processEvent, TestFail); - } - - const ControlList &controls = data.controls[0]; - if (controls.get(V4L2_CID_BRIGHTNESS).get<int32_t>() != 10 || - controls.get(V4L2_CID_CONTRAST).get<int32_t>() != 20 || - controls.get(V4L2_CID_SATURATION).get<int32_t>() != 30) { - cerr << "processEvent(): Invalid controls" << endl; - return report(Op_processEvent, TestFail); - } - - report(Op_processEvent, TestPass); - } - -private: - void report(Operation op, int status) - { - IPAOperationData data; - data.operation = op; - data.data.resize(1); - data.data[0] = status; - queueFrameAction.emit(sequence_++, data); - } - - unsigned int sequence_; -}; - -#define INVOKE(method, ...) \ - invoke(&IPAInterface::method, Op_##method, #method, ##__VA_ARGS__) - -class IPAWrappersTest : public Test -{ -public: - IPAWrappersTest() - : subdev_(nullptr), wrapper_(nullptr), sequence_(0), fd_(-1) - { - } - -protected: - int init() override - { - /* Locate the VIMC Sensor B subdevice. */ - enumerator_ = unique_ptr<DeviceEnumerator>(DeviceEnumerator::create()); - if (!enumerator_) { - cerr << "Failed to create device enumerator" << endl; - return TestFail; - } - - if (enumerator_->enumerate()) { - cerr << "Failed to enumerate media devices" << endl; - return TestFail; - } - - DeviceMatch dm("vimc"); - media_ = enumerator_->search(dm); - if (!media_) { - cerr << "No VIMC media device found: skip test" << endl; - return TestSkip; - } - - MediaEntity *entity = media_->getEntityByName("Sensor A"); - if (!entity) { - cerr << "Unable to find media entity 'Sensor A'" << endl; - return TestFail; - } - - subdev_ = new V4L2Subdevice(entity); - if (subdev_->open() < 0) { - cerr << "Unable to open 'Sensor A' subdevice" << endl; - return TestFail; - } - - /* Force usage of the C API as that's what we want to test. */ - int ret = setenv("LIBCAMERA_IPA_FORCE_C_API", "", 1); - if (ret) - return TestFail; - - std::unique_ptr<IPAInterface> intf = std::make_unique<TestIPAInterface>(); - wrapper_ = new IPAContextWrapper(new IPAInterfaceWrapper(std::move(intf))); - wrapper_->queueFrameAction.connect(this, &IPAWrappersTest::queueFrameAction); - - /* Create a file descriptor for the buffer-related operations. */ - fd_ = open("/tmp", O_TMPFILE | O_RDWR, 0600); - if (fd_ == -1) - return TestFail; - - ret = ftruncate(fd_, 4096); - if (ret < 0) - return TestFail; - - return TestPass; - } - - int run() override - { - int ret; - - /* Test configure(). */ - std::map<unsigned int, IPAStream> config{ - { 1, { V4L2_PIX_FMT_YUYV, { 1024, 768 } } }, - { 2, { V4L2_PIX_FMT_NV12, { 800, 600 } } }, - }; - std::map<unsigned int, const ControlInfoMap &> controlInfo; - controlInfo.emplace(42, subdev_->controls()); - ret = INVOKE(configure, config, controlInfo); - if (ret == TestFail) - return TestFail; - - /* Test mapBuffers(). */ - std::vector<IPABuffer> buffers(2); - buffers[0].planes.resize(3); - buffers[0].id = 10; - buffers[0].planes[0].fd = FileDescriptor(fd_); - buffers[0].planes[0].length = 4096; - buffers[1].id = 11; - buffers[1].planes.resize(3); - buffers[1].planes[0].fd = FileDescriptor(fd_); - buffers[1].planes[0].length = 4096; - buffers[1].planes[1].fd = FileDescriptor(fd_); - buffers[1].planes[1].length = 4096; - - ret = INVOKE(mapBuffers, buffers); - if (ret == TestFail) - return TestFail; - - /* Test unmapBuffers(). */ - std::vector<unsigned int> bufferIds = { 10, 11 }; - ret = INVOKE(unmapBuffers, bufferIds); - if (ret == TestFail) - return TestFail; - - /* Test processEvent(). */ - IPAOperationData data; - data.operation = Op_processEvent; - data.data = { 1, 2, 3, 4 }; - data.controls.emplace_back(subdev_->controls()); - - ControlList &controls = data.controls.back(); - controls.set(V4L2_CID_BRIGHTNESS, static_cast<int32_t>(10)); - controls.set(V4L2_CID_CONTRAST, static_cast<int32_t>(20)); - controls.set(V4L2_CID_SATURATION, static_cast<int32_t>(30)); - - ret = INVOKE(processEvent, data); - if (ret == TestFail) - return TestFail; - - /* - * Test init() last to ensure nothing in the wrappers or - * serializer depends on init() being called first. - */ - ret = INVOKE(init); - if (ret == TestFail) - return TestFail; - - return TestPass; - } - - void cleanup() override - { - delete wrapper_; - delete subdev_; - - if (fd_ != -1) - close(fd_); - } - -private: - template<typename T, typename... Args1, typename... Args2> - int invoke(T (IPAInterface::*func)(Args1...), Operation op, - const char *name, Args2... args) - { - data_ = IPAOperationData(); - (wrapper_->*func)(args...); - - if (frame_ != sequence_) { - cerr << "IPAInterface::" << name - << "(): invalid frame number " << frame_ - << ", expected " << sequence_; - return TestFail; - } - - sequence_++; - - if (data_.operation != op) { - cerr << "IPAInterface::" << name - << "(): failed to propagate" << endl; - return TestFail; - } - - if (data_.data[0] != TestPass) { - cerr << "IPAInterface::" << name - << "(): reported an error" << endl; - return TestFail; - } - - return TestPass; - } - - void queueFrameAction(unsigned int frame, const IPAOperationData &data) - { - frame_ = frame; - data_ = data; - } - - std::shared_ptr<MediaDevice> media_; - std::unique_ptr<DeviceEnumerator> enumerator_; - V4L2Subdevice *subdev_; - - IPAContextWrapper *wrapper_; - IPAOperationData data_; - unsigned int sequence_; - unsigned int frame_; - int fd_; -}; - -TEST_REGISTER(IPAWrappersTest) diff --git a/test/ipa/libipa/fixedpoint.cpp b/test/ipa/libipa/fixedpoint.cpp new file mode 100644 index 00000000..99eb662d --- /dev/null +++ b/test/ipa/libipa/fixedpoint.cpp @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com> + * + * Fixed / Floating point utility tests + */ + +#include <cmath> +#include <iostream> +#include <map> +#include <stdint.h> + +#include "../src/ipa/libipa/fixedpoint.h" + +#include "test.h" + +using namespace std; +using namespace libcamera; +using namespace ipa; + +class FixedPointUtilsTest : public Test +{ +protected: + /* R for real, I for integer */ + template<unsigned int IntPrec, unsigned int FracPrec, typename I, typename R> + int testFixedToFloat(I input, R expected) + { + R out = fixedToFloatingPoint<IntPrec, FracPrec, R>(input); + R prec = 1.0 / (1 << FracPrec); + if (std::abs(out - expected) > prec) { + cerr << "Reverse conversion expected " << input + << " to convert to " << expected + << ", got " << out << std::endl; + return TestFail; + } + + return TestPass; + } + + template<unsigned int IntPrec, unsigned int FracPrec, typename T> + int testSingleFixedPoint(double input, T expected) + { + T ret = floatingToFixedPoint<IntPrec, FracPrec, T>(input); + if (ret != expected) { + cerr << "Expected " << input << " to convert to " + << expected << ", got " << ret << std::endl; + return TestFail; + } + + /* + * The precision check is fairly arbitrary but is based on what + * the rkisp1 is capable of in the crosstalk module. + */ + double f = fixedToFloatingPoint<IntPrec, FracPrec, double>(ret); + if (std::abs(f - input) > 0.005) { + cerr << "Reverse conversion expected " << ret + << " to convert to " << input + << ", got " << f << std::endl; + return TestFail; + } + + return TestPass; + } + + int testFixedPoint() + { + /* + * The second 7.992 test is to test that unused bits don't + * affect the result. + */ + std::map<double, uint16_t> testCases = { + { 7.992, 0x3ff }, + { 0.2, 0x01a }, + { -0.2, 0x7e6 }, + { -0.8, 0x79a }, + { -0.4, 0x7cd }, + { -1.4, 0x74d }, + { -8, 0x400 }, + { 0, 0 }, + }; + + int ret; + for (const auto &testCase : testCases) { + ret = testSingleFixedPoint<4, 7, uint16_t>(testCase.first, + testCase.second); + if (ret != TestPass) + return ret; + } + + /* Special case with a superfluous one in the unused bits */ + ret = testFixedToFloat<4, 7, uint16_t, double>(0xbff, 7.992); + if (ret != TestPass) + return ret; + + return TestPass; + } + + int run() + { + /* fixed point conversion test */ + if (testFixedPoint() != TestPass) + return TestFail; + + return TestPass; + } +}; + +TEST_REGISTER(FixedPointUtilsTest) diff --git a/test/ipa/libipa/interpolator.cpp b/test/ipa/libipa/interpolator.cpp new file mode 100644 index 00000000..6abb7760 --- /dev/null +++ b/test/ipa/libipa/interpolator.cpp @@ -0,0 +1,54 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2024, Ideas on Board Oy + * + * Interpolator tests + */ + +#include "../src/ipa/libipa/interpolator.h" + +#include <cmath> +#include <iostream> +#include <map> +#include <stdint.h> +#include <stdio.h> + +#include "test.h" + +using namespace std; +using namespace libcamera; +using namespace ipa; + +#define ASSERT_EQ(a, b) \ + if ((a) != (b)) { \ + printf(#a " != " #b "\n"); \ + return TestFail; \ + } + +class InterpolatorTest : public Test +{ +protected: + int run() + { + Interpolator<int> interpolator; + interpolator.setData({ { 10, 100 }, { 20, 200 }, { 30, 300 } }); + + ASSERT_EQ(interpolator.getInterpolated(0), 100); + ASSERT_EQ(interpolator.getInterpolated(10), 100); + ASSERT_EQ(interpolator.getInterpolated(20), 200); + ASSERT_EQ(interpolator.getInterpolated(25), 250); + ASSERT_EQ(interpolator.getInterpolated(30), 300); + ASSERT_EQ(interpolator.getInterpolated(40), 300); + + interpolator.setQuantization(10); + unsigned int q = 0; + ASSERT_EQ(interpolator.getInterpolated(25, &q), 300); + ASSERT_EQ(q, 30); + ASSERT_EQ(interpolator.getInterpolated(24, &q), 200); + ASSERT_EQ(q, 20); + + return TestPass; + } +}; + +TEST_REGISTER(InterpolatorTest) diff --git a/test/ipa/libipa/meson.build b/test/ipa/libipa/meson.build new file mode 100644 index 00000000..0f4155d2 --- /dev/null +++ b/test/ipa/libipa/meson.build @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: CC0-1.0 + +libipa_test = [ + {'name': 'fixedpoint', 'sources': ['fixedpoint.cpp']}, + {'name': 'interpolator', 'sources': ['interpolator.cpp']}, + {'name': 'vector', 'sources': ['vector.cpp']}, +] + +foreach test : libipa_test + exe = executable(test['name'], test['sources'], + dependencies : [libcamera_private, libipa_dep], + implicit_include_directories : false, + link_with : [test_libraries], + include_directories : [test_includes_internal, + '../../../src/ipa/libipa/']) + + test(test['name'], exe, suite : 'ipa') +endforeach diff --git a/test/ipa/libipa/vector.cpp b/test/ipa/libipa/vector.cpp new file mode 100644 index 00000000..8e4ec77d --- /dev/null +++ b/test/ipa/libipa/vector.cpp @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2024, Ideas on Board Oy + * + * Vector tests + */ + +#include "../src/ipa/libipa/vector.h" + +#include <cmath> +#include <iostream> + +#include "test.h" + +using namespace libcamera::ipa; + +#define ASSERT_EQ(a, b) \ +if ((a) != (b)) { \ + std::cout << #a " != " #b << " (line " << __LINE__ << ")" \ + << std::endl; \ + return TestFail; \ +} + +class VectorTest : public Test +{ +protected: + int run() + { + Vector<double, 3> v1{ 0.0 }; + + ASSERT_EQ(v1[0], 0.0); + ASSERT_EQ(v1[1], 0.0); + ASSERT_EQ(v1[2], 0.0); + + ASSERT_EQ(v1.length(), 0.0); + ASSERT_EQ(v1.length2(), 0.0); + + Vector<double, 3> v2{{ 1.0, 4.0, 8.0 }}; + + ASSERT_EQ(v2[0], 1.0); + ASSERT_EQ(v2[1], 4.0); + ASSERT_EQ(v2[2], 8.0); + + ASSERT_EQ(v2.x(), 1.0); + ASSERT_EQ(v2.y(), 4.0); + ASSERT_EQ(v2.z(), 8.0); + + ASSERT_EQ(v2.r(), 1.0); + ASSERT_EQ(v2.g(), 4.0); + ASSERT_EQ(v2.b(), 8.0); + + ASSERT_EQ(v2.length2(), 81.0); + ASSERT_EQ(v2.length(), 9.0); + ASSERT_EQ(v2.sum(), 13.0); + + Vector<double, 3> v3{ v2 }; + + ASSERT_EQ(v2, v3); + + v3 = Vector<double, 3>{{ 4.0, 4.0, 4.0 }}; + + ASSERT_EQ(v2 + v3, (Vector<double, 3>{{ 5.0, 8.0, 12.0 }})); + ASSERT_EQ(v2 + 4.0, (Vector<double, 3>{{ 5.0, 8.0, 12.0 }})); + ASSERT_EQ(v2 - v3, (Vector<double, 3>{{ -3.0, 0.0, 4.0 }})); + ASSERT_EQ(v2 - 4.0, (Vector<double, 3>{{ -3.0, 0.0, 4.0 }})); + ASSERT_EQ(v2 * v3, (Vector<double, 3>{{ 4.0, 16.0, 32.0 }})); + ASSERT_EQ(v2 * 4.0, (Vector<double, 3>{{ 4.0, 16.0, 32.0 }})); + ASSERT_EQ(v2 / v3, (Vector<double, 3>{{ 0.25, 1.0, 2.0 }})); + ASSERT_EQ(v2 / 4.0, (Vector<double, 3>{{ 0.25, 1.0, 2.0 }})); + + ASSERT_EQ(v2.min(v3), (Vector<double, 3>{{ 1.0, 4.0, 4.0 }})); + ASSERT_EQ(v2.min(4.0), (Vector<double, 3>{{ 1.0, 4.0, 4.0 }})); + ASSERT_EQ(v2.max(v3), (Vector<double, 3>{{ 4.0, 4.0, 8.0 }})); + ASSERT_EQ(v2.max(4.0), (Vector<double, 3>{{ 4.0, 4.0, 8.0 }})); + + ASSERT_EQ(v2.dot(v3), 52.0); + + v2 += v3; + ASSERT_EQ(v2, (Vector<double, 3>{{ 5.0, 8.0, 12.0 }})); + v2 -= v3; + ASSERT_EQ(v2, (Vector<double, 3>{{ 1.0, 4.0, 8.0 }})); + v2 *= v3; + ASSERT_EQ(v2, (Vector<double, 3>{{ 4.0, 16.0, 32.0 }})); + v2 /= v3; + ASSERT_EQ(v2, (Vector<double, 3>{{ 1.0, 4.0, 8.0 }})); + + v2 += 4.0; + ASSERT_EQ(v2, (Vector<double, 3>{{ 5.0, 8.0, 12.0 }})); + v2 -= 4.0; + ASSERT_EQ(v2, (Vector<double, 3>{{ 1.0, 4.0, 8.0 }})); + v2 *= 4.0; + ASSERT_EQ(v2, (Vector<double, 3>{{ 4.0, 16.0, 32.0 }})); + v2 /= 4.0; + ASSERT_EQ(v2, (Vector<double, 3>{{ 1.0, 4.0, 8.0 }})); + + return TestPass; + } +}; + +TEST_REGISTER(VectorTest) diff --git a/test/ipa/meson.build b/test/ipa/meson.build index f925c50a..ceed15ba 100644 --- a/test/ipa/meson.build +++ b/test/ipa/meson.build @@ -1,14 +1,17 @@ +# SPDX-License-Identifier: CC0-1.0 + +subdir('libipa') + ipa_test = [ - ['ipa_module_test', 'ipa_module_test.cpp'], - ['ipa_interface_test', 'ipa_interface_test.cpp'], - ['ipa_wrappers_test', 'ipa_wrappers_test.cpp'], + {'name': 'ipa_module_test', 'sources': ['ipa_module_test.cpp']}, + {'name': 'ipa_interface_test', 'sources': ['ipa_interface_test.cpp']}, ] -foreach t : ipa_test - exe = executable(t[0], t[1], - dependencies : libcamera_dep, - link_with : [libipa, test_libraries], - include_directories : [libipa_includes, test_includes_internal]) +foreach test : ipa_test + exe = executable(test['name'], test['sources'], + dependencies : [libcamera_private, libipa_dep], + link_with : [test_libraries], + include_directories : [test_includes_internal]) - test(t[0], exe, suite : 'ipa') + test(test['name'], exe, suite : 'ipa') endforeach diff --git a/test/ipc/meson.build b/test/ipc/meson.build index cc46b41c..8e447d22 100644 --- a/test/ipc/meson.build +++ b/test/ipc/meson.build @@ -1,12 +1,15 @@ +# SPDX-License-Identifier: CC0-1.0 + ipc_tests = [ - [ 'unixsocket', 'unixsocket.cpp' ], + {'name': 'unixsocket_ipc', 'sources': ['unixsocket_ipc.cpp']}, + {'name': 'unixsocket', 'sources': ['unixsocket.cpp']}, ] -foreach t : ipc_tests - exe = executable(t[0], t[1], - dependencies : libcamera_dep, +foreach test : ipc_tests + exe = executable(test['name'], test['sources'], + dependencies : libcamera_private, link_with : test_libraries, include_directories : test_includes_internal) - test(t[0], exe, suite : 'ipc') + test(test['name'], exe, suite : 'ipc') endforeach diff --git a/test/ipc/unixsocket.cpp b/test/ipc/unixsocket.cpp index f53042b8..f39bd986 100644 --- a/test/ipc/unixsocket.cpp +++ b/test/ipc/unixsocket.cpp @@ -2,10 +2,11 @@ /* * Copyright (C) 2019, Google Inc. * - * unixsocket.cpp - Unix socket IPC test + * Unix socket IPC test */ #include <algorithm> +#include <array> #include <fcntl.h> #include <iostream> #include <stdlib.h> @@ -14,14 +15,15 @@ #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> +#include <vector> -#include <libcamera/event_dispatcher.h> -#include <libcamera/timer.h> +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> + +#include "libcamera/internal/ipc_unixsocket.h" -#include "ipc_unixsocket.h" #include "test.h" -#include "thread.h" -#include "utils.h" #define CMD_CLOSE 0 #define CMD_REVERSE 1 @@ -29,8 +31,11 @@ #define CMD_LEN_CMP 3 #define CMD_JOIN 4 -using namespace std; using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; + +namespace { int calculateLength(int fd) { @@ -41,6 +46,8 @@ int calculateLength(int fd) return size; } +} /* namespace */ + class UnixSocketTestSlave { public: @@ -51,9 +58,9 @@ public: ipc_.readyRead.connect(this, &UnixSocketTestSlave::readyRead); } - int run(int fd) + int run(UniqueFD fd) { - if (ipc_.bind(fd)) { + if (ipc_.bind(std::move(fd))) { cerr << "Failed to connect to IPC channel" << endl; return EXIT_FAILURE; } @@ -67,12 +74,12 @@ public: } private: - void readyRead(IPCUnixSocket *ipc) + void readyRead() { IPCUnixSocket::Payload message, response; int ret; - ret = ipc->receive(&message); + ret = ipc_.receive(&message); if (ret) { cerr << "Receive message failed: " << ret << endl; return; @@ -145,6 +152,7 @@ private: if (num < 0) { cerr << "Read failed" << endl; + close(outfd); stop(-EIO); return; } else if (!num) @@ -152,6 +160,7 @@ private: if (write(outfd, buf, num) < 0) { cerr << "Write failed" << endl; + close(outfd); stop(-EIO); return; } @@ -206,8 +215,7 @@ protected: if (!pid_) { std::string arg = std::to_string(fd); - execl("/proc/self/exe", "/proc/self/exe", - arg.c_str(), nullptr); + execl(self().c_str(), self().c_str(), arg.c_str(), nullptr); /* Only get here if exec fails. */ exit(TestFail); @@ -309,7 +317,7 @@ protected: }; int fds[2]; - for (unsigned int i = 0; i < ARRAY_SIZE(strings); i++) { + for (unsigned int i = 0; i < std::size(strings); i++) { unsigned int len = strlen(strings[i]); fds[i] = open("/tmp", O_TMPFILE | O_RDWR, @@ -331,16 +339,16 @@ protected: if (ret) return ret; - for (unsigned int i = 0; i < ARRAY_SIZE(strings); i++) { + for (unsigned int i = 0; i < std::size(strings); i++) { unsigned int len = strlen(strings[i]); - char buf[len]; + std::vector<char> buf(len); close(fds[i]); - if (read(response.fds[0], &buf, len) <= 0) + if (read(response.fds[0], buf.data(), len) <= 0) return TestFail; - if (memcmp(buf, strings[i], len)) + if (memcmp(buf.data(), strings[i], len)) return TestFail; } @@ -357,11 +365,11 @@ protected: int run() { - int slavefd = ipc_.create(); - if (slavefd < 0) + UniqueFD slavefd = ipc_.create(); + if (!slavefd.isValid()) return TestFail; - if (slaveStart(slavefd)) { + if (slaveStart(slavefd.release())) { cerr << "Failed to start slave" << endl; return TestFail; } @@ -428,7 +436,7 @@ private: if (ret) return ret; - timeout.start(200); + timeout.start(2s); while (!callDone_) { if (!timeout.isRunning()) { cerr << "Call timeout!" << endl; @@ -444,14 +452,14 @@ private: return 0; } - void readyRead(IPCUnixSocket *ipc) + void readyRead() { if (!callResponse_) { cerr << "Read ready without expecting data, fail." << endl; return; } - if (ipc->receive(callResponse_)) { + if (ipc_.receive(callResponse_)) { cerr << "Receive message failed" << endl; return; } @@ -461,7 +469,7 @@ private: int prepareFDs(IPCUnixSocket::Payload *message, unsigned int num) { - int fd = open("/proc/self/exe", O_RDONLY); + int fd = open(self().c_str(), O_RDONLY); if (fd < 0) return fd; @@ -493,10 +501,12 @@ private: int main(int argc, char **argv) { if (argc == 2) { - int ipcfd = std::stoi(argv[1]); + UniqueFD ipcfd = UniqueFD(std::stoi(argv[1])); UnixSocketTestSlave slave; - return slave.run(ipcfd); + return slave.run(std::move(ipcfd)); } - return UnixSocketTest().execute(); + UnixSocketTest test; + test.setArgs(argc, argv); + return test.execute(); } diff --git a/test/ipc/unixsocket_ipc.cpp b/test/ipc/unixsocket_ipc.cpp new file mode 100644 index 00000000..df7d9c2b --- /dev/null +++ b/test/ipc/unixsocket_ipc.cpp @@ -0,0 +1,233 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Google Inc. + * + * Unix socket IPC test + */ + +#include <algorithm> +#include <fcntl.h> +#include <iostream> +#include <limits.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> +#include <libcamera/base/utils.h> + +#include "libcamera/internal/ipa_data_serializer.h" +#include "libcamera/internal/ipc_pipe.h" +#include "libcamera/internal/ipc_pipe_unixsocket.h" +#include "libcamera/internal/process.h" + +#include "test.h" + +using namespace std; +using namespace libcamera; + +enum { + CmdExit = 0, + CmdGetSync = 1, + CmdSetAsync = 2, +}; + +const int32_t kInitialValue = 1337; +const int32_t kChangedValue = 9001; + +class UnixSocketTestIPCSlave +{ +public: + UnixSocketTestIPCSlave() + : value_(kInitialValue), exitCode_(EXIT_FAILURE), exit_(false) + { + dispatcher_ = Thread::current()->eventDispatcher(); + ipc_.readyRead.connect(this, &UnixSocketTestIPCSlave::readyRead); + } + + int run(UniqueFD fd) + { + if (ipc_.bind(std::move(fd))) { + cerr << "Failed to connect to IPC channel" << endl; + return EXIT_FAILURE; + } + + while (!exit_) + dispatcher_->processEvents(); + + ipc_.close(); + + return exitCode_; + } + +private: + void readyRead() + { + IPCUnixSocket::Payload message; + int ret; + + ret = ipc_.receive(&message); + if (ret) { + cerr << "Receive message failed: " << ret << endl; + return; + } + + IPCMessage ipcMessage(message); + uint32_t cmd = ipcMessage.header().cmd; + + switch (cmd) { + case CmdExit: { + exit_ = true; + break; + } + + case CmdGetSync: { + IPCMessage::Header header = { cmd, ipcMessage.header().cookie }; + IPCMessage response(header); + + vector<uint8_t> buf; + tie(buf, ignore) = IPADataSerializer<int32_t>::serialize(value_); + response.data().insert(response.data().end(), buf.begin(), buf.end()); + + ret = ipc_.send(response.payload()); + if (ret < 0) { + cerr << "Reply failed" << endl; + stop(ret); + } + break; + } + + case CmdSetAsync: { + value_ = IPADataSerializer<int32_t>::deserialize(ipcMessage.data()); + break; + } + } + } + + void stop(int code) + { + exitCode_ = code; + exit_ = true; + } + + int32_t value_; + + IPCUnixSocket ipc_; + EventDispatcher *dispatcher_; + int exitCode_; + bool exit_; +}; + +class UnixSocketTestIPC : public Test +{ +protected: + int init() + { + return 0; + } + + int setValue(int32_t val) + { + IPCMessage msg(CmdSetAsync); + tie(msg.data(), ignore) = IPADataSerializer<int32_t>::serialize(val); + + int ret = ipc_->sendAsync(msg); + if (ret < 0) { + cerr << "Failed to call set value" << endl; + return ret; + } + + return 0; + } + + int getValue() + { + IPCMessage msg(CmdGetSync); + IPCMessage buf; + + int ret = ipc_->sendSync(msg, &buf); + if (ret < 0) { + cerr << "Failed to call get value" << endl; + return ret; + } + + return IPADataSerializer<int32_t>::deserialize(buf.data()); + } + + int exit() + { + IPCMessage msg(CmdExit); + + int ret = ipc_->sendAsync(msg); + if (ret < 0) { + cerr << "Failed to call exit" << endl; + return ret; + } + + return 0; + } + + int run() + { + ipc_ = std::make_unique<IPCPipeUnixSocket>("", self().c_str()); + if (!ipc_->isConnected()) { + cerr << "Failed to create IPCPipe" << endl; + return TestFail; + } + + int ret = getValue(); + if (ret != kInitialValue) { + cerr << "Wrong initial value, expected " + << kInitialValue << ", got " << ret << endl; + return TestFail; + } + + ret = setValue(kChangedValue); + if (ret < 0) { + cerr << "Failed to set value: " << strerror(-ret) << endl; + return TestFail; + } + + ret = getValue(); + if (ret != kChangedValue) { + cerr << "Wrong set value, expected " << kChangedValue + << ", got " << ret << endl; + return TestFail; + } + + ret = exit(); + if (ret < 0) { + cerr << "Failed to exit: " << strerror(-ret) << endl; + return TestFail; + } + + return TestPass; + } + +private: + ProcessManager processManager_; + + unique_ptr<IPCPipeUnixSocket> ipc_; +}; + +/* + * Can't use TEST_REGISTER() as single binary needs to act as both client and + * server + */ +int main(int argc, char **argv) +{ + /* IPCPipeUnixSocket passes IPA module path in argv[1] */ + if (argc == 3) { + UniqueFD ipcfd = UniqueFD(std::stoi(argv[2])); + UnixSocketTestIPCSlave slave; + return slave.run(std::move(ipcfd)); + } + + UnixSocketTestIPC test; + test.setArgs(argc, argv); + return test.execute(); +} diff --git a/test/libtest/buffer_source.cpp b/test/libtest/buffer_source.cpp index dae3cb9f..dde11f36 100644 --- a/test/libtest/buffer_source.cpp +++ b/test/libtest/buffer_source.cpp @@ -10,10 +10,12 @@ #include <iostream> #include <memory> -#include "device_enumerator.h" +#include "libcamera/internal/device_enumerator.h" #include "test.h" +using namespace libcamera; + BufferSource::BufferSource() { } @@ -50,7 +52,7 @@ int BufferSource::allocate(const StreamConfiguration &config) return TestSkip; } - std::unique_ptr<V4L2VideoDevice> video{ V4L2VideoDevice::fromEntityName(media_.get(), videoDeviceName) }; + std::unique_ptr<V4L2VideoDevice> video = V4L2VideoDevice::fromEntityName(media_.get(), videoDeviceName); if (!video) { std::cout << "Failed to get video device from entity " << videoDeviceName << std::endl; @@ -70,8 +72,7 @@ int BufferSource::allocate(const StreamConfiguration &config) } format.size = config.size; - format.fourcc = V4L2VideoDevice::toV4L2PixelFormat(config.pixelFormat, - false); + format.fourcc = video->toV4L2PixelFormat(config.pixelFormat); if (video->setFormat(&format)) { std::cout << "Failed to set format on output device" << std::endl; return TestFail; diff --git a/test/libtest/buffer_source.h b/test/libtest/buffer_source.h index ae0879c9..495da8a9 100644 --- a/test/libtest/buffer_source.h +++ b/test/libtest/buffer_source.h @@ -2,17 +2,15 @@ /* * Copyright (C) 2020, Google Inc. * - * buffer_source.h - libcamera camera test helper to create FrameBuffers + * libcamera camera test helper to create FrameBuffers */ -#ifndef __LIBCAMERA_BUFFER_SOURCE_TEST_H__ -#define __LIBCAMERA_BUFFER_SOURCE_TEST_H__ -#include <libcamera/libcamera.h> +#pragma once -#include "media_device.h" -#include "v4l2_videodevice.h" +#include <libcamera/stream.h> -using namespace libcamera; +#include "libcamera/internal/media_device.h" +#include "libcamera/internal/v4l2_videodevice.h" class BufferSource { @@ -20,12 +18,10 @@ public: BufferSource(); ~BufferSource(); - int allocate(const StreamConfiguration &config); - const std::vector<std::unique_ptr<FrameBuffer>> &buffers(); + int allocate(const libcamera::StreamConfiguration &config); + const std::vector<std::unique_ptr<libcamera::FrameBuffer>> &buffers(); private: - std::shared_ptr<MediaDevice> media_; - std::vector<std::unique_ptr<FrameBuffer>> buffers_; + std::shared_ptr<libcamera::MediaDevice> media_; + std::vector<std::unique_ptr<libcamera::FrameBuffer>> buffers_; }; - -#endif /* __LIBCAMERA_BUFFER_SOURCE_TEST_H__ */ diff --git a/test/libtest/camera_test.cpp b/test/libtest/camera_test.cpp index 2ae4d677..fe13d6ac 100644 --- a/test/libtest/camera_test.cpp +++ b/test/libtest/camera_test.cpp @@ -13,10 +13,13 @@ using namespace libcamera; using namespace std; -CameraTest::CameraTest(const char *name) +CameraTest::CameraTest(const char *name, bool isolate) { cm_ = new CameraManager(); + if (isolate) + setenv("LIBCAMERA_IPA_FORCE_ISOLATION", "1", 1); + if (cm_->start()) { cerr << "Failed to start camera manager" << endl; status_ = TestFail; diff --git a/test/libtest/camera_test.h b/test/libtest/camera_test.h index 0b6bad05..713b503f 100644 --- a/test/libtest/camera_test.h +++ b/test/libtest/camera_test.h @@ -2,25 +2,24 @@ /* * Copyright (C) 2019, Google Inc. * - * camera_test.h - libcamera camera test base class + * libcamera camera test base class */ -#ifndef __LIBCAMERA_CAMERA_TEST_H__ -#define __LIBCAMERA_CAMERA_TEST_H__ -#include <libcamera/libcamera.h> +#pragma once -using namespace libcamera; +#include <memory> + +#include <libcamera/camera.h> +#include <libcamera/camera_manager.h> class CameraTest { public: - CameraTest(const char *name); + CameraTest(const char *name, bool isolate = false); ~CameraTest(); protected: - CameraManager *cm_; - std::shared_ptr<Camera> camera_; + libcamera::CameraManager *cm_; + std::shared_ptr<libcamera::Camera> camera_; int status_; }; - -#endif /* __LIBCAMERA_CAMERA_TEST_H__ */ diff --git a/test/libtest/meson.build b/test/libtest/meson.build index 33565e0e..351629f3 100644 --- a/test/libtest/meson.build +++ b/test/libtest/meson.build @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: CC0-1.0 + libtest_sources = files([ 'buffer_source.cpp', 'camera_test.cpp', @@ -13,11 +15,10 @@ test_includes_public = [ test_includes_internal = [ test_includes_public, - libcamera_internal_includes, ] libtest = static_library('libtest', libtest_sources, - dependencies : libcamera_dep, + dependencies : libcamera_private, include_directories : test_includes_internal) test_libraries = [libtest] diff --git a/test/libtest/test.cpp b/test/libtest/test.cpp index fd9f3d74..4e03def9 100644 --- a/test/libtest/test.cpp +++ b/test/libtest/test.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2018, Google Inc. * - * test.cpp - libcamera test base class + * libcamera test base class */ #include <stdlib.h> @@ -17,6 +17,11 @@ Test::~Test() { } +void Test::setArgs([[maybe_unused]] int argc, char *argv[]) +{ + self_ = argv[0]; +} + int Test::execute() { int ret; diff --git a/test/libtest/test.h b/test/libtest/test.h index 26d4b94b..3a90885d 100644 --- a/test/libtest/test.h +++ b/test/libtest/test.h @@ -2,12 +2,13 @@ /* * Copyright (C) 2018, Google Inc. * - * test.h - libcamera test base class + * libcamera test base class */ -#ifndef __TEST_TEST_H__ -#define __TEST_TEST_H__ + +#pragma once #include <sstream> +#include <string> enum TestStatus { TestPass = 0, @@ -21,18 +22,24 @@ public: Test(); virtual ~Test(); + void setArgs(int argc, char *argv[]); int execute(); + const std::string &self() const { return self_; } + protected: virtual int init() { return 0; } virtual int run() = 0; virtual void cleanup() {} + +private: + std::string self_; }; -#define TEST_REGISTER(klass) \ +#define TEST_REGISTER(Klass) \ int main(int argc, char *argv[]) \ { \ - return klass().execute(); \ + Klass klass; \ + klass.setArgs(argc, argv); \ + return klass.execute(); \ } - -#endif /* __TEST_TEST_H__ */ diff --git a/test/list-cameras.cpp b/test/list-cameras.cpp deleted file mode 100644 index 55551d7e..00000000 --- a/test/list-cameras.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* - * Copyright (C) 2018, Google Inc. - * - * list.cpp - camera list tests - */ - -#include <iostream> - -#include <libcamera/camera.h> -#include <libcamera/camera_manager.h> - -#include "test.h" - -using namespace std; -using namespace libcamera; - -class ListTest : public Test -{ -protected: - int init() - { - cm_ = new CameraManager(); - cm_->start(); - - return 0; - } - - int run() - { - unsigned int count = 0; - - for (const std::shared_ptr<Camera> &camera : cm_->cameras()) { - cout << "- " << camera->name() << endl; - count++; - } - - return count ? 0 : -ENODEV; - } - - void cleanup() - { - cm_->stop(); - delete cm_; - } - -private: - CameraManager *cm_; -}; - -TEST_REGISTER(ListTest) diff --git a/test/log/log_api.cpp b/test/log/log_api.cpp index 33622f84..0b999738 100644 --- a/test/log/log_api.cpp +++ b/test/log/log_api.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * log.cpp - log API test + * log API test */ #include <algorithm> @@ -16,9 +16,10 @@ #include <sys/types.h> #include <unistd.h> +#include <libcamera/base/log.h> + #include <libcamera/logging.h> -#include "log.h" #include "test.h" using namespace std; @@ -86,6 +87,7 @@ protected: if (logSetFile(path) < 0) { cerr << "Failed to set log file" << endl; + close(fd); return TestFail; } @@ -96,6 +98,7 @@ protected: lseek(fd, 0, SEEK_SET); if (read(fd, buf, sizeof(buf)) < 0) { cerr << "Failed to read tmp log file" << endl; + close(fd); return TestFail; } close(fd); diff --git a/test/log/log_process.cpp b/test/log/log_process.cpp index 2df4aa43..9609e23d 100644 --- a/test/log/log_process.cpp +++ b/test/log/log_process.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * log_process.cpp - Logging in isolated child process test + * Logging in isolated child process test */ #include <fcntl.h> @@ -14,18 +14,21 @@ #include <unistd.h> #include <vector> -#include <libcamera/event_dispatcher.h> #include <libcamera/logging.h> -#include <libcamera/timer.h> -#include "log.h" -#include "process.h" +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/log.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> +#include <libcamera/base/utils.h> + +#include "libcamera/internal/process.h" + #include "test.h" -#include "thread.h" -#include "utils.h" -using namespace std; using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; static const string message("hello from the child"); @@ -72,18 +75,19 @@ protected: vector<std::string> args; args.push_back(to_string(exitCode)); args.push_back(to_string(num_)); - int ret = proc_.start("/proc/self/exe", args); + int ret = proc_.start(self(), args); if (ret) { cerr << "failed to start process" << endl; return TestFail; } - timeout.start(200); + timeout.start(2s); while (timeout.isRunning()) dispatcher->processEvents(); if (exitStatus_ != Process::NormalExit) { - cerr << "process did not exit normally" << endl; + cerr << "process did not exit normally: " << exitStatus_ + << endl; return TestFail; } @@ -106,13 +110,17 @@ protected: memset(buf, 0, sizeof(buf)); if (read(fd, buf, sizeof(buf)) < 0) { cerr << "Failed to read tmp log file" << endl; + close(fd); return TestFail; } close(fd); string str(buf); - if (str.find(message) == string::npos) + if (str.find(message) == string::npos) { + cerr << "Received message is not correct (received " + << str.length() << " bytes)" << endl; return TestFail; + } return TestPass; } @@ -123,14 +131,16 @@ protected: } private: - void procFinished(Process *proc, enum Process::ExitStatus exitStatus, int exitCode) + void procFinished(enum Process::ExitStatus exitStatus, int exitCode) { exitStatus_ = exitStatus; exitCode_ = exitCode; } + ProcessManager processManager_; + Process proc_; - Process::ExitStatus exitStatus_; + Process::ExitStatus exitStatus_ = Process::NotExited; string logPath_; int exitCode_; int num_; @@ -149,5 +159,7 @@ int main(int argc, char **argv) return child.run(status, num); } - return LogProcessTest().execute(); + LogProcessTest test; + test.setArgs(argc, argv); + return test.execute(); } diff --git a/test/log/meson.build b/test/log/meson.build index 95f6c1a2..2298ff84 100644 --- a/test/log/meson.build +++ b/test/log/meson.build @@ -1,13 +1,15 @@ +# SPDX-License-Identifier: CC0-1.0 + log_test = [ - ['log_api', 'log_api.cpp'], - ['log_process', 'log_process.cpp'], + {'name': 'log_api', 'sources': ['log_api.cpp']}, + {'name': 'log_process', 'sources': ['log_process.cpp']}, ] -foreach t : log_test - exe = executable(t[0], t[1], - dependencies : libcamera_dep, +foreach test : log_test + exe = executable(test['name'], test['sources'], + dependencies : libcamera_private, link_with : test_libraries, include_directories : test_includes_internal) - test(t[0], exe, suite : 'log') + test(test['name'], exe, suite : 'log') endforeach diff --git a/test/mapped-buffer.cpp b/test/mapped-buffer.cpp new file mode 100644 index 00000000..b4422f7d --- /dev/null +++ b/test/mapped-buffer.cpp @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Google Inc. + * + * libcamera internal MappedBuffer tests + */ + +#include <iostream> + +#include <libcamera/framebuffer_allocator.h> + +#include "libcamera/internal/mapped_framebuffer.h" + +#include "camera_test.h" +#include "test.h" + +using namespace libcamera; +using namespace std; + +namespace { + +class MappedBufferTest : public CameraTest, public Test +{ +public: + MappedBufferTest() + : CameraTest("platform/vimc.0 Sensor B") + { + } + +protected: + int init() override + { + if (status_ != TestPass) + return status_; + + config_ = camera_->generateConfiguration({ StreamRole::VideoRecording }); + if (!config_ || config_->size() != 1) { + cout << "Failed to generate default configuration" << endl; + return TestFail; + } + + allocator_ = new FrameBufferAllocator(camera_); + + StreamConfiguration &cfg = config_->at(0); + + if (camera_->acquire()) { + cout << "Failed to acquire the camera" << endl; + return TestFail; + } + + if (camera_->configure(config_.get())) { + cout << "Failed to set default configuration" << endl; + return TestFail; + } + + stream_ = cfg.stream(); + + int ret = allocator_->allocate(stream_); + if (ret < 0) + return TestFail; + + return TestPass; + } + + void cleanup() override + { + delete allocator_; + } + + int run() override + { + const std::unique_ptr<FrameBuffer> &buffer = allocator_->buffers(stream_).front(); + std::vector<MappedBuffer> maps; + + MappedFrameBuffer map(buffer.get(), MappedFrameBuffer::MapFlag::Read); + if (!map.isValid()) { + cout << "Failed to successfully map buffer" << endl; + return TestFail; + } + + /* Make sure we can move it. */ + maps.emplace_back(std::move(map)); + + /* But copying is prevented, it would cause double-unmap. */ + // MappedFrameBuffer map_copy = map; + + /* Local map should be invalid (after move). */ + if (map.isValid()) { + cout << "Post-move map should not be valid" << endl; + return TestFail; + } + + /* Test for multiple successful maps on the same buffer. */ + MappedFrameBuffer write_map(buffer.get(), MappedFrameBuffer::MapFlag::Write); + if (!write_map.isValid()) { + cout << "Failed to map write buffer" << endl; + return TestFail; + } + + MappedFrameBuffer rw_map(buffer.get(), MappedFrameBuffer::MapFlag::ReadWrite); + if (!rw_map.isValid()) { + cout << "Failed to map RW buffer" << endl; + return TestFail; + } + + return TestPass; + } + +private: + std::unique_ptr<CameraConfiguration> config_; + FrameBufferAllocator *allocator_; + Stream *stream_; +}; + +} /* namespace */ + +TEST_REGISTER(MappedBufferTest) diff --git a/test/media_device/media_device_acquire.cpp b/test/media_device/media_device_acquire.cpp index d1e3d744..371e30e9 100644 --- a/test/media_device/media_device_acquire.cpp +++ b/test/media_device/media_device_acquire.cpp @@ -30,4 +30,4 @@ class MediaDeviceAcquire : public MediaDeviceTest } }; -TEST_REGISTER(MediaDeviceAcquire); +TEST_REGISTER(MediaDeviceAcquire) diff --git a/test/media_device/media_device_link_test.cpp b/test/media_device/media_device_link_test.cpp index fe7542bb..31528000 100644 --- a/test/media_device/media_device_link_test.cpp +++ b/test/media_device/media_device_link_test.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * media_device_link_test.cpp - Tests link handling on VIMC media device + * Tests link handling on VIMC media device */ #include <iostream> @@ -56,7 +56,7 @@ class MediaDeviceLinkTest : public MediaDeviceTest /* * Test if link can be consistently retrieved through the - * different methods the media device offers. + * different functions the media device offers. */ string linkName("'Debayer A':[1] -> 'Scaler':[0]'"); MediaLink *link = media_->link("Debayer A", 1, "Scaler", 0); @@ -219,4 +219,4 @@ class MediaDeviceLinkTest : public MediaDeviceTest } }; -TEST_REGISTER(MediaDeviceLinkTest); +TEST_REGISTER(MediaDeviceLinkTest) diff --git a/test/media_device/media_device_print_test.cpp b/test/media_device/media_device_print_test.cpp index 5018906c..63aeed48 100644 --- a/test/media_device/media_device_print_test.cpp +++ b/test/media_device/media_device_print_test.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2018-2019, Google Inc. * - * media_device_print_test.cpp - Print out media devices + * Print out media devices */ #include <iostream> @@ -10,7 +10,7 @@ #include <sys/stat.h> #include <unistd.h> -#include "media_device.h" +#include "libcamera/internal/media_device.h" #include "test.h" @@ -25,10 +25,6 @@ using namespace std; */ class MediaDevicePrintTest : public Test { -public: - MediaDevicePrintTest() {} - ~MediaDevicePrintTest() {} - protected: int init() { return 0; } int run(); @@ -151,4 +147,4 @@ int MediaDevicePrintTest::run() return ret; } -TEST_REGISTER(MediaDevicePrintTest); +TEST_REGISTER(MediaDevicePrintTest) diff --git a/test/media_device/media_device_test.cpp b/test/media_device/media_device_test.cpp index 1397d123..3e41d0f0 100644 --- a/test/media_device/media_device_test.cpp +++ b/test/media_device/media_device_test.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * media_device_test.cpp - libcamera media device test base class + * libcamera media device test base class */ #include <iostream> diff --git a/test/media_device/media_device_test.h b/test/media_device/media_device_test.h index cdbd1484..5223b760 100644 --- a/test/media_device/media_device_test.h +++ b/test/media_device/media_device_test.h @@ -2,20 +2,18 @@ /* * Copyright (C) 2019, Google Inc. * - * media_device_test.h - libcamera media device test base class + * libcamera media device test base class */ -#ifndef __LIBCAMERA_MEDIADEVICE_TEST_H__ -#define __LIBCAMERA_MEDIADEVICE_TEST_H__ + +#pragma once #include <memory> -#include "device_enumerator.h" -#include "media_device.h" +#include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/media_device.h" #include "test.h" -using namespace libcamera; - class MediaDeviceTest : public Test { public: @@ -25,10 +23,8 @@ public: protected: int init(); - std::shared_ptr<MediaDevice> media_; + std::shared_ptr<libcamera::MediaDevice> media_; private: - std::unique_ptr<DeviceEnumerator> enumerator_; + std::unique_ptr<libcamera::DeviceEnumerator> enumerator_; }; - -#endif /* __LIBCAMERA_MEDIADEVICE_TEST_H__ */ diff --git a/test/media_device/meson.build b/test/media_device/meson.build index 6a0e4684..84966c97 100644 --- a/test/media_device/meson.build +++ b/test/media_device/meson.build @@ -1,22 +1,24 @@ +# SPDX-License-Identifier: CC0-1.0 + lib_mdev_test_sources = files([ 'media_device_test.cpp', ]) media_device_tests = [ - ['media_device_acquire', 'media_device_acquire.cpp'], - ['media_device_print_test', 'media_device_print_test.cpp'], - ['media_device_link_test', 'media_device_link_test.cpp'], + {'name': 'media_device_acquire', 'sources': ['media_device_acquire.cpp']}, + {'name': 'media_device_print_test', 'sources': ['media_device_print_test.cpp']}, + {'name': 'media_device_link_test', 'sources': ['media_device_link_test.cpp']}, ] lib_mdev_test = static_library('lib_mdev_test', lib_mdev_test_sources, - dependencies : libcamera_dep, + dependencies : libcamera_private, include_directories : test_includes_internal) -foreach t : media_device_tests - exe = executable(t[0], t[1], - dependencies : libcamera_dep, +foreach test : media_device_tests + exe = executable(test['name'], test['sources'], + dependencies : libcamera_private, link_with : [test_libraries, lib_mdev_test], include_directories : test_includes_internal) - test(t[0], exe, suite : 'media_device', is_parallel : false) + test(test['name'], exe, suite : 'media_device', is_parallel : false) endforeach diff --git a/test/meson.build b/test/meson.build index 8ab58ac1..5ed052ed 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,56 +1,129 @@ +# SPDX-License-Identifier: CC0-1.0 + +if not get_option('test') + test_enabled = false + subdir_done() +endif + +test_enabled = true + +# When ASan is enabled, find the path to the ASan runtime needed by multiple +# tests. This currently works with gcc only, as clang uses different file names +# depending on the compiler version and target architecture. +asan_enabled = false +asan_runtime_missing = false + +if get_option('b_sanitize').contains('address') + asan_enabled = true + + if cc.get_id() == 'gcc' + asan_runtime = run_command(cc, '-print-file-name=libasan.so', check : true).stdout().strip() + else + asan_runtime_missing = true + endif +endif + subdir('libtest') subdir('camera') subdir('controls') +subdir('gstreamer') subdir('ipa') subdir('ipc') subdir('log') subdir('media_device') -subdir('pipeline') subdir('process') +subdir('py') subdir('serialization') subdir('stream') +subdir('v4l2_compat') subdir('v4l2_subdevice') subdir('v4l2_videodevice') public_tests = [ - ['geometry', 'geometry.cpp'], - ['list-cameras', 'list-cameras.cpp'], - ['signal', 'signal.cpp'], - ['span', 'span.cpp'], + {'name': 'color-space', 'sources': ['color-space.cpp']}, + {'name': 'geometry', 'sources': ['geometry.cpp']}, + {'name': 'public-api', 'sources': ['public-api.cpp']}, + {'name': 'signal', 'sources': ['signal.cpp']}, + {'name': 'span', 'sources': ['span.cpp']}, + {'name': 'transform', 'sources': ['transform.cpp']}, ] internal_tests = [ - ['byte-stream-buffer', 'byte-stream-buffer.cpp'], - ['camera-sensor', 'camera-sensor.cpp'], - ['event', 'event.cpp'], - ['event-dispatcher', 'event-dispatcher.cpp'], - ['event-thread', 'event-thread.cpp'], - ['file-descriptor', 'file-descriptor.cpp'], - ['message', 'message.cpp'], - ['object', 'object.cpp'], - ['object-invoke', 'object-invoke.cpp'], - ['signal-threads', 'signal-threads.cpp'], - ['threads', 'threads.cpp'], - ['timer', 'timer.cpp'], - ['timer-thread', 'timer-thread.cpp'], - ['utils', 'utils.cpp'], + {'name': 'bayer-format', 'sources': ['bayer-format.cpp']}, + {'name': 'byte-stream-buffer', 'sources': ['byte-stream-buffer.cpp']}, + {'name': 'camera-sensor', 'sources': ['camera-sensor.cpp']}, + {'name': 'delayed_controls', 'sources': ['delayed_controls.cpp']}, + {'name': 'event', 'sources': ['event.cpp']}, + {'name': 'event-dispatcher', 'sources': ['event-dispatcher.cpp']}, + {'name': 'event-thread', 'sources': ['event-thread.cpp']}, + {'name': 'file', 'sources': ['file.cpp']}, + {'name': 'flags', 'sources': ['flags.cpp']}, + {'name': 'hotplug-cameras', 'sources': ['hotplug-cameras.cpp']}, + {'name': 'message', 'sources': ['message.cpp']}, + {'name': 'object', 'sources': ['object.cpp']}, + {'name': 'object-delete', 'sources': ['object-delete.cpp']}, + {'name': 'object-invoke', 'sources': ['object-invoke.cpp']}, + {'name': 'pixel-format', 'sources': ['pixel-format.cpp']}, + {'name': 'shared-fd', 'sources': ['shared-fd.cpp']}, + {'name': 'signal-threads', 'sources': ['signal-threads.cpp']}, + {'name': 'threads', 'sources': 'threads.cpp', 'dependencies': [libthreads]}, + {'name': 'timer', 'sources': ['timer.cpp']}, + {'name': 'timer-fail', 'sources': ['timer-fail.cpp'], 'should_fail': true}, + {'name': 'timer-thread', 'sources': ['timer-thread.cpp']}, + {'name': 'unique-fd', 'sources': ['unique-fd.cpp']}, + {'name': 'utils', 'sources': ['utils.cpp']}, + {'name': 'yaml-parser', 'sources': ['yaml-parser.cpp']}, +] + +internal_non_parallel_tests = [ + {'name': 'fence', 'sources': ['fence.cpp']}, + {'name': 'mapped-buffer', 'sources': ['mapped-buffer.cpp']}, ] -foreach t : public_tests - exe = executable(t[0], t[1], - dependencies : libcamera_dep, +foreach test : public_tests + deps = [libcamera_public] + if 'dependencies' in test + deps += test['dependencies'] + endif + + exe = executable(test['name'], test['sources'], + dependencies : deps, + implicit_include_directories : false, link_with : test_libraries, include_directories : test_includes_public) - test(t[0], exe) + test(test['name'], exe, should_fail : test.get('should_fail', false)) +endforeach + +foreach test : internal_tests + deps = [libcamera_private] + if 'dependencies' in test + deps += test['dependencies'] + endif + + exe = executable(test['name'], test['sources'], + dependencies : deps, + implicit_include_directories : false, + link_with : test_libraries, + include_directories : test_includes_internal) + + test(test['name'], exe, should_fail : test.get('should_fail', false)) endforeach -foreach t : internal_tests - exe = executable(t[0], t[1], - dependencies : libcamera_dep, +foreach test : internal_non_parallel_tests + deps = [libcamera_private] + if 'dependencies' in test + deps += test['dependencies'] + endif + + exe = executable(test['name'], test['sources'], + dependencies : deps, + implicit_include_directories : false, link_with : test_libraries, include_directories : test_includes_internal) - test(t[0], exe) + test(test['name'], exe, + is_parallel : false, + should_fail : test.get('should_fail', false)) endforeach diff --git a/test/message.cpp b/test/message.cpp index 478bc79d..19e6646d 100644 --- a/test/message.cpp +++ b/test/message.cpp @@ -2,15 +2,18 @@ /* * Copyright (C) 2019, Google Inc. * - * message.cpp - Messages test + * Messages test */ #include <chrono> #include <iostream> +#include <memory> #include <thread> -#include "message.h" -#include "thread.h" +#include <libcamera/base/message.h> +#include <libcamera/base/object.h> +#include <libcamera/base/thread.h> + #include "test.h" using namespace std; @@ -25,8 +28,8 @@ public: MessageReceived, }; - MessageReceiver() - : status_(NoMessage) + MessageReceiver(Object *parent = nullptr) + : Object(parent), status_(NoMessage) { } @@ -51,23 +54,43 @@ private: Status status_; }; -class SlowMessageReceiver : public Object +class RecursiveMessageReceiver : public Object { +public: + RecursiveMessageReceiver() + : child_(this), success_(false) + { + } + + bool success() const { return success_; } + protected: - void message(Message *msg) + void message([[maybe_unused]] Message *msg) { if (msg->type() != Message::None) { Object::message(msg); return; } + child_.postMessage(std::make_unique<Message>(Message::None)); + /* - * Don't access any member of the object here (including the - * vtable) as the object will be deleted by the main thread - * while we're sleeping. + * If the child has already received the message, something is + * wrong. */ - this_thread::sleep_for(chrono::milliseconds(100)); + if (child_.status() != MessageReceiver::NoMessage) + return; + + Thread::current()->dispatchMessages(Message::None); + + /* The child should now have received the message. */ + if (child_.status() == MessageReceiver::MessageReceived) + success_ = true; } + +private: + MessageReceiver child_; + bool success_; }; class MessageTest : public Test @@ -86,16 +109,19 @@ protected: return TestFail; } - MessageReceiver receiver; - receiver.moveToThread(&thread_); + MessageReceiver *receiver = new MessageReceiver(); + receiver->moveToThread(&thread_); thread_.start(); - receiver.postMessage(std::make_unique<Message>(Message::None)); + receiver->postMessage(std::make_unique<Message>(Message::None)); this_thread::sleep_for(chrono::milliseconds(100)); - switch (receiver.status()) { + MessageReceiver::Status status = receiver->status(); + receiver->deleteLater(); + + switch (status) { case MessageReceiver::NoMessage: cout << "No message received" << endl; return TestFail; @@ -107,17 +133,26 @@ protected: } /* - * Test for races between message delivery and object deletion. - * Failures result in assertion errors, there is no need for - * explicit checks. + * Test recursive calls to Thread::dispatchMessages(). Messages + * should be delivered correctly, without crashes or memory + * leaks. Two messages need to be posted to ensure we don't only + * test the simple case of a queue containing a single message. */ - SlowMessageReceiver *slowReceiver = new SlowMessageReceiver(); - slowReceiver->moveToThread(&thread_); - slowReceiver->postMessage(std::make_unique<Message>(Message::None)); + RecursiveMessageReceiver *recursiveReceiver = new RecursiveMessageReceiver(); + recursiveReceiver->moveToThread(&thread_); + + recursiveReceiver->postMessage(std::make_unique<Message>(Message::None)); + recursiveReceiver->postMessage(std::make_unique<Message>(Message::UserMessage)); this_thread::sleep_for(chrono::milliseconds(10)); - delete slowReceiver; + bool success = recursiveReceiver->success(); + recursiveReceiver->deleteLater(); + + if (!success) { + cout << "Recursive message delivery failed" << endl; + return TestFail; + } return TestPass; } diff --git a/test/object-delete.cpp b/test/object-delete.cpp new file mode 100644 index 00000000..676c3970 --- /dev/null +++ b/test/object-delete.cpp @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Google Inc. + * + * Object deletion tests + */ + +#include <iostream> + +#include <libcamera/base/object.h> +#include <libcamera/base/thread.h> + +#include "test.h" + +using namespace std; +using namespace libcamera; + +class TestObject : public Object +{ +public: + TestObject(unsigned int *count) + : deleteCount_(count) + { + } + + ~TestObject() + { + /* Count the deletions from the correct thread. */ + if (thread() == Thread::current()) + (*deleteCount_)++; + } + + unsigned int *deleteCount_; +}; + +class DeleterThread : public Thread +{ +public: + DeleterThread(Object *obj) + : object_(obj) + { + } + +protected: + void run() + { + object_->deleteLater(); + } + +private: + Object *object_; +}; + +class ObjectDeleteTest : public Test +{ +protected: + int run() + { + /* + * Test that deferred deletion is executed from the object's + * thread, not the caller's thread. + */ + unsigned int count = 0; + TestObject *obj = new TestObject(&count); + + DeleterThread delThread(obj); + delThread.start(); + delThread.wait(); + + Thread::current()->dispatchMessages(Message::Type::DeferredDelete); + + if (count != 1) { + cout << "Failed to dispatch DeferredDelete (" << count << ")" << endl; + return TestFail; + } + + /* + * Test that multiple calls to deleteLater() delete the object + * once only. + */ + count = 0; + obj = new TestObject(&count); + obj->deleteLater(); + obj->deleteLater(); + + Thread::current()->dispatchMessages(Message::Type::DeferredDelete); + if (count != 1) { + cout << "Multiple deleteLater() failed (" << count << ")" << endl; + return TestFail; + } + + /* + * Test that deleteLater() works properly when called just + * before the object's thread exits. + */ + Thread boundThread; + boundThread.start(); + + count = 0; + obj = new TestObject(&count); + obj->moveToThread(&boundThread); + + obj->deleteLater(); + boundThread.exit(); + boundThread.wait(); + + if (count != 1) { + cout << "Object deletion right before thread exit failed (" << count << ")" << endl; + return TestFail; + } + + return TestPass; + } +}; + +TEST_REGISTER(ObjectDeleteTest) diff --git a/test/object-invoke.cpp b/test/object-invoke.cpp index fa162c83..def1e61e 100644 --- a/test/object-invoke.cpp +++ b/test/object-invoke.cpp @@ -2,17 +2,17 @@ /* * Copyright (C) 2019, Google Inc. * - * object-invoke.cpp - Cross-thread Object method invocation test + * Cross-thread Object method invocation test */ #include <iostream> #include <thread> -#include <libcamera/event_dispatcher.h> -#include <libcamera/object.h> +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/object.h> +#include <libcamera/base/thread.h> #include "test.h" -#include "thread.h" using namespace std; using namespace libcamera; @@ -49,7 +49,7 @@ public: value_ = value; } - void methodWithReference(const int &value) + void methodWithReference([[maybe_unused]] const int &value) { } diff --git a/test/object.cpp b/test/object.cpp index 16118971..95dc1ef3 100644 --- a/test/object.cpp +++ b/test/object.cpp @@ -2,15 +2,14 @@ /* * Copyright (C) 2019, Google Inc. * - * object.cpp - Object tests + * Object tests */ #include <iostream> -#include <libcamera/object.h> - -#include "message.h" -#include "thread.h" +#include <libcamera/base/message.h> +#include <libcamera/base/object.h> +#include <libcamera/base/thread.h> #include "test.h" diff --git a/test/pipeline/ipu3/ipu3_pipeline_test.cpp b/test/pipeline/ipu3/ipu3_pipeline_test.cpp deleted file mode 100644 index a5c6be09..00000000 --- a/test/pipeline/ipu3/ipu3_pipeline_test.cpp +++ /dev/null @@ -1,125 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* - * Copyright (C) 2019, Google Inc. - * - * ipu3_pipeline_test.cpp - Intel IPU3 pipeline test - */ - -#include <iostream> -#include <sys/stat.h> -#include <sys/types.h> -#include <unistd.h> - -#include <libcamera/camera.h> -#include <libcamera/camera_manager.h> - -#include "device_enumerator.h" -#include "media_device.h" -#include "media_object.h" -#include "test.h" - -using namespace std; -using namespace libcamera; - -/* - * Verify that the Intel IPU3 pipeline handler gets matched and cameras - * are enumerated correctly. - * - * The test is supposed to be run on an IPU3 platform, otherwise it gets - * skipped. - * - * The test lists all cameras registered in the system, if any camera is - * available at all. - */ -class IPU3PipelineTest : public Test -{ -protected: - int init(); - int run(); - void cleanup(); - -private: - CameraManager *cameraManager_; - unsigned int sensors_; -}; - -int IPU3PipelineTest::init() -{ - unique_ptr<DeviceEnumerator> enumerator = DeviceEnumerator::create(); - if (!enumerator) { - cerr << "Failed to create device enumerator" << endl; - return TestFail; - } - - if (enumerator->enumerate()) { - cerr << "Failed to enumerate media devices" << endl; - return TestFail; - } - - DeviceMatch imgu_dm("ipu3-imgu"); - DeviceMatch cio2_dm("ipu3-cio2"); - - if (!enumerator->search(imgu_dm)) { - cerr << "Failed to find IPU3 IMGU: test skip" << endl; - return TestSkip; - } - - std::shared_ptr<MediaDevice> cio2 = enumerator->search(cio2_dm); - if (!cio2) { - cerr << "Failed to find IPU3 CIO2: test skip" << endl; - return TestSkip; - } - - /* - * Camera sensor are connected to the CIO2 unit. - * Count how many sensors are connected in the system - * and later verify this matches the number of registered - * cameras. - */ - int ret = cio2->populate(); - if (ret) { - cerr << "Failed to populate media device " << cio2->deviceNode() << endl; - return TestFail; - } - - sensors_ = 0; - const vector<MediaEntity *> &entities = cio2->entities(); - for (MediaEntity *entity : entities) { - if (entity->function() == MEDIA_ENT_F_CAM_SENSOR) - sensors_++; - } - - enumerator.reset(nullptr); - - cameraManager_ = new CameraManager(); - ret = cameraManager_->start(); - if (ret) { - cerr << "Failed to start the CameraManager" << endl; - return TestFail; - } - - return 0; -} - -int IPU3PipelineTest::run() -{ - auto cameras = cameraManager_->cameras(); - for (const std::shared_ptr<Camera> &cam : cameras) - cout << "Found camera '" << cam->name() << "'" << endl; - - if (cameras.size() != sensors_) { - cerr << cameras.size() << " cameras registered, but " << sensors_ - << " were expected" << endl; - return TestFail; - } - - return TestPass; -} - -void IPU3PipelineTest::cleanup() -{ - cameraManager_->stop(); - delete cameraManager_; -} - -TEST_REGISTER(IPU3PipelineTest) diff --git a/test/pipeline/ipu3/meson.build b/test/pipeline/ipu3/meson.build deleted file mode 100644 index d02927c9..00000000 --- a/test/pipeline/ipu3/meson.build +++ /dev/null @@ -1,12 +0,0 @@ -ipu3_test = [ - ['ipu3_pipeline_test', 'ipu3_pipeline_test.cpp'], -] - -foreach t : ipu3_test - exe = executable(t[0], t[1], - dependencies : libcamera_dep, - link_with : test_libraries, - include_directories : test_includes_internal) - - test(t[0], exe, suite : 'ipu3', is_parallel : false) -endforeach diff --git a/test/pipeline/meson.build b/test/pipeline/meson.build deleted file mode 100644 index 157f789c..00000000 --- a/test/pipeline/meson.build +++ /dev/null @@ -1,2 +0,0 @@ -subdir('ipu3') -subdir('rkisp1') diff --git a/test/pipeline/rkisp1/meson.build b/test/pipeline/rkisp1/meson.build deleted file mode 100644 index d3f97496..00000000 --- a/test/pipeline/rkisp1/meson.build +++ /dev/null @@ -1,12 +0,0 @@ -rkisp1_test = [ - ['rkisp1_pipeline_test', 'rkisp1_pipeline_test.cpp'], -] - -foreach t : rkisp1_test - exe = executable(t[0], t[1], - dependencies : libcamera_dep, - link_with : test_libraries, - include_directories : test_includes_internal) - - test(t[0], exe, suite : 'rkisp1', is_parallel : false) -endforeach diff --git a/test/pipeline/rkisp1/rkisp1_pipeline_test.cpp b/test/pipeline/rkisp1/rkisp1_pipeline_test.cpp deleted file mode 100644 index d46c928f..00000000 --- a/test/pipeline/rkisp1/rkisp1_pipeline_test.cpp +++ /dev/null @@ -1,114 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* - * Copyright (C) 2020, Linaro - * - * Based on test/pipeline/ipu3/ipu3_pipeline_test.cpp - * - * rkisp1_pipeline_test.cpp - Rockchip RK3399 rkisp1 pipeline test - */ - -#include <iostream> - -#include <sys/stat.h> -#include <sys/types.h> -#include <unistd.h> - -#include <libcamera/camera.h> -#include <libcamera/camera_manager.h> - -#include "device_enumerator.h" -#include "media_device.h" -#include "media_object.h" -#include "test.h" - -using namespace std; -using namespace libcamera; - -/* - * Verify that the RK3399 pipeline handler gets matched and cameras - * are enumerated correctly. - * - * The test is supposed to be run on rockchip platform. - * - * The test lists all cameras registered in the system, if any camera is - * available at all. - */ -class RKISP1PipelineTest : public Test -{ -protected: - int init(); - int run(); - void cleanup(); - -private: - CameraManager *cameraManager_; - unsigned int sensors_; -}; - -int RKISP1PipelineTest::init() -{ - unique_ptr<DeviceEnumerator> enumerator = DeviceEnumerator::create(); - if (!enumerator) { - cerr << "Failed to create device enumerator" << endl; - return TestFail; - } - - if (enumerator->enumerate()) { - cerr << "Failed to enumerate media devices" << endl; - return TestFail; - } - - DeviceMatch dm("rkisp1"); - - std::shared_ptr<MediaDevice> rkisp1 = enumerator->search(dm); - if (!rkisp1) { - cerr << "Failed to find rkisp1: test skip" << endl; - return TestSkip; - } - - int ret = rkisp1->populate(); - if (ret) { - cerr << "Failed to populate media device " - << rkisp1->deviceNode() << endl; - return TestFail; - } - - sensors_ = 0; - const vector<MediaEntity *> &entities = rkisp1->entities(); - for (MediaEntity *entity : entities) { - if (entity->function() == MEDIA_ENT_F_CAM_SENSOR) - sensors_++; - } - - cameraManager_ = new CameraManager(); - ret = cameraManager_->start(); - if (ret) { - cerr << "Failed to start the CameraManager" << endl; - return TestFail; - } - - return 0; -} - -int RKISP1PipelineTest::run() -{ - auto cameras = cameraManager_->cameras(); - for (const std::shared_ptr<Camera> &cam : cameras) - cout << "Found camera '" << cam->name() << "'" << endl; - - if (cameras.size() != sensors_) { - cerr << cameras.size() << " cameras registered, but " << sensors_ - << " were expected" << endl; - return TestFail; - } - - return TestPass; -} - -void RKISP1PipelineTest::cleanup() -{ - cameraManager_->stop(); - delete cameraManager_; -} - -TEST_REGISTER(RKISP1PipelineTest) diff --git a/test/pixel-format.cpp b/test/pixel-format.cpp new file mode 100644 index 00000000..0f364f83 --- /dev/null +++ b/test/pixel-format.cpp @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Kaaira Gupta + * libcamera pixel format handling test + */ + +#include <iostream> +#include <vector> + +#include <libcamera/formats.h> +#include <libcamera/pixel_format.h> + +#include <libcamera/base/utils.h> + +#include "test.h" + +using namespace std; +using namespace libcamera; + +class PixelFormatTest : public Test +{ +protected: + int run() + { + std::vector<std::pair<PixelFormat, const char *>> formatsMap{ + { formats::R8, "R8" }, + { formats::SRGGB10_CSI2P, "SRGGB10_CSI2P" }, + { PixelFormat(0, 0), "<INVALID>" }, + { PixelFormat(0x20203843), "<C8 >" } + }; + + for (const auto &format : formatsMap) { + if ((format.first).toString() != format.second) { + cerr << "Failed to convert PixelFormat " + << utils::hex(format.first.fourcc()) << " to string" + << endl; + return TestFail; + } + } + + if (PixelFormat().toString() != "<INVALID>") { + cerr << "Failed to convert default PixelFormat to string" + << endl; + return TestFail; + } + + return TestPass; + } +}; + +TEST_REGISTER(PixelFormatTest) diff --git a/test/process/meson.build b/test/process/meson.build index c4d83d6c..a80dc2d9 100644 --- a/test/process/meson.build +++ b/test/process/meson.build @@ -1,12 +1,14 @@ +# SPDX-License-Identifier: CC0-1.0 + process_tests = [ - [ 'process_test', 'process_test.cpp' ], + {'name': 'process_test', 'sources': ['process_test.cpp']}, ] -foreach t : process_tests - exe = executable(t[0], t[1], - dependencies : libcamera_dep, +foreach test : process_tests + exe = executable(test['name'], test['sources'], + dependencies : libcamera_private, link_with : test_libraries, include_directories : test_includes_internal) - test(t[0], exe, suite : 'process', is_parallel : false) + test(test['name'], exe, suite : 'process', is_parallel : false) endforeach diff --git a/test/process/process_test.cpp b/test/process/process_test.cpp index 7e7b3c2c..e9f5e7e9 100644 --- a/test/process/process_test.cpp +++ b/test/process/process_test.cpp @@ -2,23 +2,25 @@ /* * Copyright (C) 2019, Google Inc. * - * process_test.cpp - Process test + * Process test */ #include <iostream> #include <unistd.h> #include <vector> -#include <libcamera/event_dispatcher.h> -#include <libcamera/timer.h> +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> +#include <libcamera/base/utils.h> + +#include "libcamera/internal/process.h" -#include "process.h" #include "test.h" -#include "thread.h" -#include "utils.h" -using namespace std; using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; class ProcessTestChild { @@ -50,13 +52,17 @@ protected: args.push_back(to_string(exitCode)); proc_.finished.connect(this, &ProcessTest::procFinished); - int ret = proc_.start("/proc/self/exe", args); + /* Test that kill() on an unstarted process is safe. */ + proc_.kill(); + + /* Test starting the process and retrieving the exit code. */ + int ret = proc_.start(self(), args); if (ret) { cerr << "failed to start process" << endl; return TestFail; } - timeout.start(2000); + timeout.start(2000ms); while (timeout.isRunning() && exitStatus_ == Process::NotExited) dispatcher->processEvents(); @@ -75,12 +81,14 @@ protected: } private: - void procFinished(Process *proc, enum Process::ExitStatus exitStatus, int exitCode) + void procFinished(enum Process::ExitStatus exitStatus, int exitCode) { exitStatus_ = exitStatus; exitCode_ = exitCode; } + ProcessManager processManager_; + Process proc_; enum Process::ExitStatus exitStatus_; int exitCode_; @@ -98,5 +106,7 @@ int main(int argc, char **argv) return child.run(status); } - return ProcessTest().execute(); + ProcessTest test; + test.setArgs(argc, argv); + return test.execute(); } diff --git a/test/public-api.cpp b/test/public-api.cpp new file mode 100644 index 00000000..b1336f75 --- /dev/null +++ b/test/public-api.cpp @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Google Inc. + * + * Public API validation + */ + +#include <libcamera/libcamera.h> + +#include "test.h" + +class PublicAPITest : public Test +{ + int run() + { +#ifdef LIBCAMERA_BASE_PRIVATE +#error "Public interfaces should not be exposed to LIBCAMERA_BASE_PRIVATE" + return TestFail; +#else + return TestPass; +#endif + } +}; + +TEST_REGISTER(PublicAPITest) diff --git a/test/py/meson.build b/test/py/meson.build new file mode 100644 index 00000000..b922e857 --- /dev/null +++ b/test/py/meson.build @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: CC0-1.0 + +if not pycamera_enabled + subdir_done() +endif + +# If ASan is enabled, the link order runtime check will fail as Python is not +# linked to ASan. LD_PRELOAD the ASan runtime if available, or skip the test +# otherwise. + +if asan_runtime_missing + warning('Unable to get path to ASan runtime, Python test disabled') + subdir_done() +endif + +py_env = environment() + +pymod = import('python') +py3 = pymod.find_installation('python3') + +pypathdir = meson.project_build_root() / 'src' / 'py' +py_env.append('PYTHONPATH', pypathdir) + +if asan_enabled + py_env.append('LD_PRELOAD', asan_runtime) + + # Preload the C++ standard library to work around a bug in ASan when + # dynamically loading C++ .so modules. + stdlib = run_command(cxx, '-print-file-name=' + cxx_stdlib + '.so', + check : true).stdout().strip() + py_env.append('LD_PRELOAD', stdlib) + + # Disable leak detection as the Python interpreter is full of leaks. + py_env.append('ASAN_OPTIONS', 'detect_leaks=0') +endif + +test('pyunittests', + py3, + args : files('unittests.py'), + env : py_env, + suite : 'pybindings', + is_parallel : false) diff --git a/test/py/unittests.py b/test/py/unittests.py new file mode 100755 index 00000000..8cb850d4 --- /dev/null +++ b/test/py/unittests.py @@ -0,0 +1,365 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + +from collections import defaultdict +import gc +import libcamera as libcam +import selectors +import typing +import unittest +import weakref + + +class BaseTestCase(unittest.TestCase): + def assertZero(self, a, msg=None): + self.assertEqual(a, 0, msg) + + def assertIsAlive(self, wr, msg='object not alive'): + self.assertIsNotNone(wr(), msg) + + def assertIsDead(self, wr, msg='object not dead'): + self.assertIsNone(wr(), msg) + + def assertIsAllAlive(self, wr_list, msg='object not alive'): + self.assertTrue(all([wr() for wr in wr_list]), msg) + + def assertIsAllDead(self, wr_list, msg='object not dead'): + self.assertTrue(all([not wr() for wr in wr_list]), msg) + + +class SimpleTestMethods(BaseTestCase): + def test_get_ref(self): + cm = libcam.CameraManager.singleton() + wr_cm = weakref.ref(cm) + + cam = cm.get('platform/vimc.0 Sensor B') + self.assertIsNotNone(cam) + wr_cam = weakref.ref(cam) + + del cm + gc.collect() + self.assertIsAlive(wr_cm) + + del cam + gc.collect() + self.assertIsDead(wr_cm) + self.assertIsDead(wr_cam) + + def test_acquire_release(self): + cm = libcam.CameraManager.singleton() + cam = cm.get('platform/vimc.0 Sensor B') + self.assertIsNotNone(cam) + + cam.acquire() + + cam.release() + + def test_double_acquire(self): + cm = libcam.CameraManager.singleton() + cam = cm.get('platform/vimc.0 Sensor B') + self.assertIsNotNone(cam) + + cam.acquire() + + libcam.log_set_level('Camera', 'FATAL') + with self.assertRaises(RuntimeError): + cam.acquire() + libcam.log_set_level('Camera', 'INFO') + + cam.release() + + # I expected exception here, but looks like double release works fine + cam.release() + + def test_version(self): + cm = libcam.CameraManager.singleton() + self.assertIsInstance(cm.version, str) + + +class CameraTesterBase(BaseTestCase): + cm: typing.Any + cam: typing.Any + + def setUp(self): + self.cm = libcam.CameraManager.singleton() + self.cam = next((cam for cam in self.cm.cameras if 'platform/vimc' in cam.id), None) + if self.cam is None: + self.cm = None + self.skipTest('No vimc found') + + self.cam.acquire() + + self.wr_cam = weakref.ref(self.cam) + self.wr_cm = weakref.ref(self.cm) + + def tearDown(self): + # If a test fails, the camera may be in running state. So always stop. + self.cam.stop() + + self.cam.release() + + self.cam = None + self.cm = None + + self.assertIsDead(self.wr_cm) + self.assertIsDead(self.wr_cam) + + +class AllocatorTestMethods(CameraTesterBase): + def test_allocator(self): + cam = self.cam + + camconfig = cam.generate_configuration([libcam.StreamRole.StillCapture]) + self.assertTrue(camconfig.size == 1) + wr_camconfig = weakref.ref(camconfig) + + streamconfig = camconfig.at(0) + wr_streamconfig = weakref.ref(streamconfig) + + cam.configure(camconfig) + + stream = streamconfig.stream + wr_stream = weakref.ref(stream) + + # stream should keep streamconfig and camconfig alive + del streamconfig + del camconfig + gc.collect() + self.assertIsAlive(wr_camconfig) + self.assertIsAlive(wr_streamconfig) + + allocator = libcam.FrameBufferAllocator(cam) + num_bufs = allocator.allocate(stream) + self.assertTrue(num_bufs > 0) + wr_allocator = weakref.ref(allocator) + + buffers = allocator.buffers(stream) + self.assertIsNotNone(buffers) + del buffers + + buffer = allocator.buffers(stream)[0] + self.assertIsNotNone(buffer) + wr_buffer = weakref.ref(buffer) + + del allocator + gc.collect() + self.assertIsAlive(wr_buffer) + self.assertIsAlive(wr_allocator) + self.assertIsAlive(wr_stream) + + del buffer + gc.collect() + self.assertIsDead(wr_buffer) + self.assertIsDead(wr_allocator) + self.assertIsAlive(wr_stream) + + del stream + gc.collect() + self.assertIsDead(wr_stream) + self.assertIsDead(wr_camconfig) + self.assertIsDead(wr_streamconfig) + + +class SimpleCaptureMethods(CameraTesterBase): + def test_blocking(self): + cm = self.cm + cam = self.cam + + camconfig = cam.generate_configuration([libcam.StreamRole.StillCapture]) + self.assertTrue(camconfig.size == 1) + + streamconfig = camconfig.at(0) + fmts = streamconfig.formats + self.assertIsNotNone(fmts) + fmts = None + + cam.configure(camconfig) + + stream = streamconfig.stream + + allocator = libcam.FrameBufferAllocator(cam) + num_bufs = allocator.allocate(stream) + self.assertTrue(num_bufs > 0) + + num_bufs = len(allocator.buffers(stream)) + + reqs = [] + for i in range(num_bufs): + req = cam.create_request(i) + self.assertIsNotNone(req) + + buffer = allocator.buffers(stream)[i] + req.add_buffer(stream, buffer) + + reqs.append(req) + + buffer = None + + cam.start() + + for req in reqs: + cam.queue_request(req) + + reqs = None + gc.collect() + + sel = selectors.DefaultSelector() + sel.register(cm.event_fd, selectors.EVENT_READ) + + reqs = [] + + while True: + events = sel.select() + if not events: + continue + + ready_reqs = cm.get_ready_requests() + + reqs += ready_reqs + + if len(reqs) == num_bufs: + break + + for i, req in enumerate(reqs): + self.assertTrue(i == req.cookie) + + reqs = None + gc.collect() + + cam.stop() + + def test_select(self): + cm = self.cm + cam = self.cam + + camconfig = cam.generate_configuration([libcam.StreamRole.StillCapture]) + self.assertTrue(camconfig.size == 1) + + streamconfig = camconfig.at(0) + fmts = streamconfig.formats + self.assertIsNotNone(fmts) + fmts = None + + cam.configure(camconfig) + + stream = streamconfig.stream + + allocator = libcam.FrameBufferAllocator(cam) + num_bufs = allocator.allocate(stream) + self.assertTrue(num_bufs > 0) + + num_bufs = len(allocator.buffers(stream)) + + reqs = [] + for i in range(num_bufs): + req = cam.create_request(i) + self.assertIsNotNone(req) + + buffer = allocator.buffers(stream)[i] + req.add_buffer(stream, buffer) + + reqs.append(req) + + buffer = None + + cam.start() + + for req in reqs: + cam.queue_request(req) + + reqs = None + gc.collect() + + sel = selectors.DefaultSelector() + sel.register(cm.event_fd, selectors.EVENT_READ) + + reqs = [] + + running = True + while running: + events = sel.select() + for _ in events: + ready_reqs = cm.get_ready_requests() + + reqs += ready_reqs + + if len(reqs) == num_bufs: + running = False + + self.assertTrue(len(reqs) == num_bufs) + + for i, req in enumerate(reqs): + self.assertTrue(i == req.cookie) + + reqs = None + gc.collect() + + cam.stop() + + +# Recursively expand slist's objects into olist, using seen to track already +# processed objects. +def _getr(slist, olist, seen): + for e in slist: + if id(e) in seen: + continue + seen.add(id(e)) + olist.append(e) + tl = gc.get_referents(e) + if tl: + _getr(tl, olist, seen) + + +def get_all_objects(ignored=[]): + gcl = gc.get_objects() + olist = [] + seen = set() + + seen.add(id(gcl)) + seen.add(id(olist)) + seen.add(id(seen)) + seen.update(set([id(o) for o in ignored])) + + _getr(gcl, olist, seen) + + return olist + + +def create_type_count_map(olist): + map = defaultdict(int) + for o in olist: + map[type(o)] += 1 + return map + + +def diff_type_count_maps(before, after): + return [(k, after[k] - before[k]) for k in after if after[k] != before[k]] + + +if __name__ == '__main__': + # \todo This is an attempt to see the Python objects that are not collected, + # but this doesn't work very well, as things always leak a bit. + test_leaks = False + + if test_leaks: + gc.unfreeze() + gc.collect() + + obs_before = get_all_objects() + + unittest.main(exit=False) + + if test_leaks: + gc.unfreeze() + gc.collect() + + obs_after = get_all_objects([obs_before]) # type: ignore + + before = create_type_count_map(obs_before) # type: ignore + after = create_type_count_map(obs_after) + + leaks = diff_type_count_maps(before, after) + if len(leaks) > 0: + print(leaks) diff --git a/test/serialization/control_serialization.cpp b/test/serialization/control_serialization.cpp index 2989b527..06c572b7 100644 --- a/test/serialization/control_serialization.cpp +++ b/test/serialization/control_serialization.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * control_serialization.cpp - Serialize and deserialize controls + * Serialize and deserialize controls */ #include <iostream> @@ -11,8 +11,9 @@ #include <libcamera/control_ids.h> #include <libcamera/controls.h> -#include "byte_stream_buffer.h" -#include "control_serializer.h" +#include "libcamera/internal/byte_stream_buffer.h" +#include "libcamera/internal/control_serializer.h" + #include "serialization_test.h" #include "test.h" @@ -29,8 +30,8 @@ protected: int run() override { - ControlSerializer serializer; - ControlSerializer deserializer; + ControlSerializer serializer(ControlSerializer::Role::Proxy); + ControlSerializer deserializer(ControlSerializer::Role::Worker); std::vector<uint8_t> infoData; std::vector<uint8_t> listData; @@ -42,9 +43,9 @@ protected: const ControlInfoMap &infoMap = camera_->controls(); ControlList list(infoMap); - list.set(controls::Brightness, 255); - list.set(controls::Contrast, 128); - list.set(controls::Saturation, 50); + list.set(controls::Brightness, 0.5f); + list.set(controls::Contrast, 1.2f); + list.set(controls::Saturation, 0.2f); /* * Serialize the control list, this should fail as the control @@ -139,6 +140,15 @@ protected: return TestFail; } + /* Make sure control limits looked up by id are not changed. */ + const ControlInfo &newLimits = newInfoMap.at(&controls::Brightness); + const ControlInfo &initialLimits = infoMap.at(&controls::Brightness); + if (newLimits.min() != initialLimits.min() || + newLimits.max() != initialLimits.max()) { + cerr << "The brightness control limits have changed" << endl; + return TestFail; + } + /* Deserialize the control list and verify the contents. */ buffer = ByteStreamBuffer(const_cast<const uint8_t *>(listData.data()), listData.size()); diff --git a/test/serialization/generated_serializer/generated_serializer_test.cpp b/test/serialization/generated_serializer/generated_serializer_test.cpp new file mode 100644 index 00000000..dd696885 --- /dev/null +++ b/test/serialization/generated_serializer/generated_serializer_test.cpp @@ -0,0 +1,182 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Google Inc. + * + * Test generated serializer + */ + +#include <algorithm> +#include <iostream> +#include <tuple> +#include <vector> + +#include "test.h" + +#include "test_ipa_interface.h" +#include "test_ipa_serializer.h" + +using namespace std; +using namespace libcamera; + +class IPAGeneratedSerializerTest : public Test +{ +protected: + int init() override + { + return TestPass; + } + + int run() override + { + +#define TEST_FIELD_EQUALITY(struct1, struct2, field) \ +if (struct1.field != struct2.field) { \ + cerr << #field << " field incorrect: expected \"" \ + << t.field << "\", got \"" << u.field << "\"" << endl;\ + return TestFail; \ +} + +#define TEST_SCOPED_ENUM_EQUALITY(struct1, struct2, field) \ +if (struct1.field != struct2.field) { \ + cerr << #field << " field incorrect" << endl; \ + return TestFail; \ +} + + + ipa::test::TestStruct t, u; + + t.m = { + { "a", "z" }, + { "b", "z" }, + { "c", "z" }, + { "d", "z" }, + { "e", "z" }, + }; + + t.a = { "a", "b", "c", "d", "e" }; + + t.s1 = "hello world"; + t.s2 = "goodbye"; + t.s3 = "lorem ipsum"; + t.i = 58527; + t.c = ipa::test::IPAOperationInit; + t.e = ipa::test::ErrorFlags::Error1; + + Flags<ipa::test::ErrorFlags> flags; + flags |= ipa::test::ErrorFlags::Error1; + flags |= ipa::test::ErrorFlags::Error2; + t.f = flags; + + std::vector<uint8_t> serialized; + + std::tie(serialized, ignore) = + IPADataSerializer<ipa::test::TestStruct>::serialize(t); + + u = IPADataSerializer<ipa::test::TestStruct>::deserialize(serialized); + + if (!equals(t.m, u.m)) + return TestFail; + + if (!equals(t.a, u.a)) + return TestFail; + + TEST_FIELD_EQUALITY(t, u, s1); + TEST_FIELD_EQUALITY(t, u, s2); + TEST_FIELD_EQUALITY(t, u, s3); + TEST_FIELD_EQUALITY(t, u, i); + TEST_FIELD_EQUALITY(t, u, c); + + TEST_SCOPED_ENUM_EQUALITY(t, u, e); + TEST_SCOPED_ENUM_EQUALITY(t, u, f); + + /* Test vector of generated structs */ + std::vector<ipa::test::TestStruct> v = { t, u }; + std::vector<ipa::test::TestStruct> w; + + std::tie(serialized, ignore) = + IPADataSerializer<vector<ipa::test::TestStruct>>::serialize(v); + + w = IPADataSerializer<vector<ipa::test::TestStruct>>::deserialize(serialized); + + if (!equals(v[0].m, w[0].m) || + !equals(v[1].m, w[1].m)) + return TestFail; + + if (!equals(v[0].a, w[0].a) || + !equals(v[1].a, w[1].a)) + return TestFail; + + TEST_FIELD_EQUALITY(v[0], w[0], s1); + TEST_FIELD_EQUALITY(v[0], w[0], s2); + TEST_FIELD_EQUALITY(v[0], w[0], s3); + TEST_FIELD_EQUALITY(v[0], w[0], i); + TEST_FIELD_EQUALITY(v[0], w[0], c); + + TEST_SCOPED_ENUM_EQUALITY(v[0], w[0], e); + TEST_SCOPED_ENUM_EQUALITY(v[0], w[0], f); + + TEST_FIELD_EQUALITY(v[1], w[1], s1); + TEST_FIELD_EQUALITY(v[1], w[1], s2); + TEST_FIELD_EQUALITY(v[1], w[1], s3); + TEST_FIELD_EQUALITY(v[1], w[1], i); + TEST_FIELD_EQUALITY(v[1], w[1], c); + + TEST_SCOPED_ENUM_EQUALITY(v[1], w[1], e); + TEST_SCOPED_ENUM_EQUALITY(v[1], w[1], f); + + return TestPass; + } + +private: + bool equals(const map<string, string> &lhs, const map<string, string> &rhs) + { + bool eq = lhs.size() == rhs.size() && + equal(lhs.begin(), lhs.end(), rhs.begin(), + [](auto &a, auto &b) { return a.first == b.first && + a.second == b.second; }); + + if (eq) + return true; + + cerr << "lhs:" << endl; + for (const auto &pair : lhs) + cerr << "- " << pair.first << ": " + << pair.second << endl; + + cerr << "rhs:" << endl; + for (const auto &pair : rhs) + cerr << "- " << pair.first << ": " + << pair.second << endl; + + return false; + } + + bool equals(const vector<string> &lhs, const vector<string> &rhs) + { + bool eq = lhs.size() == rhs.size(); + + if (!eq) { + cerr << "sizes not equal" << endl; + return false; + } + + for (unsigned int i = 0; i < lhs.size(); i++) + if (lhs[i] != rhs[i]) + eq = false; + + if (eq) + return true; + + cerr << "lhs:" << endl; + for (const auto &str : lhs) + cerr << "- " << str << endl; + + cerr << "rhs:" << endl; + for (const auto &str : rhs) + cerr << "- " << str << endl; + + return false; + } +}; + +TEST_REGISTER(IPAGeneratedSerializerTest) diff --git a/test/serialization/generated_serializer/include/libcamera/ipa/meson.build b/test/serialization/generated_serializer/include/libcamera/ipa/meson.build new file mode 100644 index 00000000..ae08e9be --- /dev/null +++ b/test/serialization/generated_serializer/include/libcamera/ipa/meson.build @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: CC0-1.0 + +# test.mojom-module +mojom = custom_target('test_mojom_module', + input : 'test.mojom', + output : 'test.mojom-module', + command : [ + mojom_parser, + '--output-root', meson.project_build_root(), + '--input-root', meson.project_source_root(), + '--mojoms', '@INPUT@' + ], + env : py_build_env) + +# test_ipa_interface.h +generated_test_header = custom_target('test_ipa_interface_h', + input : mojom, + output : 'test_ipa_interface.h', + depends : mojom_templates, + command : [ + mojom_generator, 'generate', + '-g', 'libcamera', + '--bytecode_path', mojom_templates_dir, + '--libcamera_generate_header', + '--libcamera_output_path=@OUTPUT@', + './' +'@INPUT@' + ], + env : py_build_env) + +# test_ipa_serializer.h +generated_test_serializer = custom_target('test_ipa_serializer_h', + input : mojom, + output : 'test_ipa_serializer.h', + depends : mojom_templates, + command : [ + mojom_generator, 'generate', + '-g', 'libcamera', + '--bytecode_path', mojom_templates_dir, + '--libcamera_generate_serializer', + '--libcamera_output_path=@OUTPUT@', + './' +'@INPUT@' + ], + env : py_build_env) diff --git a/test/serialization/generated_serializer/include/libcamera/ipa/test.mojom b/test/serialization/generated_serializer/include/libcamera/ipa/test.mojom new file mode 100644 index 00000000..91c31642 --- /dev/null +++ b/test/serialization/generated_serializer/include/libcamera/ipa/test.mojom @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +module ipa.test; + +enum IPAOperationCode { + IPAOperationNone, + IPAOperationInit, + IPAOperationStart, + IPAOperationStop, +}; + +[scopedEnum] enum ErrorFlags { + Error1 = 0x1, + Error2 = 0x2, + Error3 = 0x4, + Error4 = 0x8, +}; + +struct IPASettings {}; + +struct TestStruct { + map<string, string> m; + array<string> a; + string s1; + string s2; + int32 i; + string s3; + IPAOperationCode c; + ErrorFlags e; + [flags] ErrorFlags f; +}; + +interface IPATestInterface { + init(IPASettings settings) => (int32 ret); + start() => (int32 ret); + stop(); + + test(TestStruct s); +}; + +interface IPATestEventInterface { + dummyEvent(uint32 val); +}; diff --git a/test/serialization/generated_serializer/meson.build b/test/serialization/generated_serializer/meson.build new file mode 100644 index 00000000..9fb9cd1d --- /dev/null +++ b/test/serialization/generated_serializer/meson.build @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: CC0-1.0 + +subdir('include/libcamera/ipa') + +exe = executable('generated_serializer_test', + [ + 'generated_serializer_test.cpp', + generated_test_header, + generated_test_serializer, + ], + dependencies : libcamera_private, + link_with : test_libraries, + include_directories : [ + test_includes_internal, + './include', + ]) + +test('generated_serializer_test', exe, + suite : 'generated_serializer', is_parallel : false) diff --git a/test/serialization/ipa_data_serializer_test.cpp b/test/serialization/ipa_data_serializer_test.cpp new file mode 100644 index 00000000..aea63c73 --- /dev/null +++ b/test/serialization/ipa_data_serializer_test.cpp @@ -0,0 +1,436 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2020, Google Inc. + * + * Test serializing/deserializing with IPADataSerializer + */ + +#include <algorithm> +#include <cxxabi.h> +#include <fcntl.h> +#include <iostream> +#include <limits> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <tuple> +#include <unistd.h> +#include <vector> + +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> + +#include "libcamera/internal/ipa_data_serializer.h" + +#include "serialization_test.h" +#include "test.h" + +using namespace std; +using namespace libcamera; + +static const ControlInfoMap Controls = ControlInfoMap({ + { &controls::AeEnable, ControlInfo(false, true) }, + { &controls::ExposureTime, ControlInfo(0, 999999) }, + { &controls::AnalogueGain, ControlInfo(1.0f, 32.0f) }, + { &controls::ColourGains, ControlInfo(0.0f, 32.0f) }, + { &controls::Brightness, ControlInfo(-1.0f, 1.0f) }, + }, controls::controls); + +namespace libcamera { + +static bool operator==(const ControlInfoMap &lhs, const ControlInfoMap &rhs) +{ + return SerializationTest::equals(lhs, rhs); +} + +} /* namespace libcamera */ + +template<typename T> +int testPodSerdes(T in) +{ + std::vector<uint8_t> buf; + std::vector<SharedFD> fds; + + std::tie(buf, fds) = IPADataSerializer<T>::serialize(in); + T out = IPADataSerializer<T>::deserialize(buf, fds); + if (in == out) + return TestPass; + + char *name = abi::__cxa_demangle(typeid(T).name(), nullptr, + nullptr, nullptr); + cerr << "Deserialized " << name << " doesn't match original" << endl; + free(name); + return TestFail; +} + +template<typename T> +int testVectorSerdes(const std::vector<T> &in, + ControlSerializer *cs = nullptr) +{ + std::vector<uint8_t> buf; + std::vector<SharedFD> fds; + + std::tie(buf, fds) = IPADataSerializer<std::vector<T>>::serialize(in, cs); + std::vector<T> out = IPADataSerializer<std::vector<T>>::deserialize(buf, fds, cs); + if (in == out) + return TestPass; + + char *name = abi::__cxa_demangle(typeid(T).name(), nullptr, + nullptr, nullptr); + cerr << "Deserialized std::vector<" << name + << "> doesn't match original" << endl; + free(name); + return TestFail; +} + +template<typename K, typename V> +int testMapSerdes(const std::map<K, V> &in, + ControlSerializer *cs = nullptr) +{ + std::vector<uint8_t> buf; + std::vector<SharedFD> fds; + + std::tie(buf, fds) = IPADataSerializer<std::map<K, V>>::serialize(in, cs); + std::map<K, V> out = IPADataSerializer<std::map<K, V>>::deserialize(buf, fds, cs); + if (in == out) + return TestPass; + + char *nameK = abi::__cxa_demangle(typeid(K).name(), nullptr, + nullptr, nullptr); + char *nameV = abi::__cxa_demangle(typeid(V).name(), nullptr, + nullptr, nullptr); + cerr << "Deserialized std::map<" << nameK << ", " << nameV + << "> doesn't match original" << endl; + free(nameK); + free(nameV); + return TestFail; +} + +class IPADataSerializerTest : public CameraTest, public Test +{ +public: + IPADataSerializerTest() + : CameraTest("platform/vimc.0 Sensor B") + { + } + +protected: + int init() override + { + return status_; + } + + int run() override + { + int ret; + + ret = testControls(); + if (ret != TestPass) + return ret; + + ret = testVector(); + if (ret != TestPass) + return ret; + + ret = testMap(); + if (ret != TestPass) + return ret; + + ret = testPod(); + if (ret != TestPass) + return ret; + + return TestPass; + } + +private: + ControlList generateControlList(const ControlInfoMap &infoMap) + { + /* Create a control list with three controls. */ + ControlList list(infoMap); + + list.set(controls::Brightness, 0.5f); + list.set(controls::Contrast, 1.2f); + list.set(controls::Saturation, 0.2f); + + return list; + } + + int testControls() + { + ControlSerializer cs(ControlSerializer::Role::Proxy); + + const ControlInfoMap &infoMap = camera_->controls(); + ControlList list = generateControlList(infoMap); + + std::vector<uint8_t> infoMapBuf; + std::tie(infoMapBuf, std::ignore) = + IPADataSerializer<ControlInfoMap>::serialize(infoMap, &cs); + + std::vector<uint8_t> listBuf; + std::tie(listBuf, std::ignore) = + IPADataSerializer<ControlList>::serialize(list, &cs); + + const ControlInfoMap infoMapOut = + IPADataSerializer<ControlInfoMap>::deserialize(infoMapBuf, &cs); + + ControlList listOut = IPADataSerializer<ControlList>::deserialize(listBuf, &cs); + + if (!SerializationTest::equals(infoMap, infoMapOut)) { + cerr << "Deserialized map doesn't match original" << endl; + return TestFail; + } + + if (!SerializationTest::equals(list, listOut)) { + cerr << "Deserialized list doesn't match original" << endl; + return TestFail; + } + + return TestPass; + } + + int testVector() + { + ControlSerializer cs(ControlSerializer::Role::Proxy); + + /* + * We don't test SharedFD serdes because it dup()s, so we + * can't check for equality. + */ + std::vector<uint8_t> vecUint8 = { 1, 2, 3, 4, 5, 6 }; + std::vector<uint16_t> vecUint16 = { 1, 2, 3, 4, 5, 6 }; + std::vector<uint32_t> vecUint32 = { 1, 2, 3, 4, 5, 6 }; + std::vector<uint64_t> vecUint64 = { 1, 2, 3, 4, 5, 6 }; + std::vector<int8_t> vecInt8 = { 1, 2, 3, -4, 5, -6 }; + std::vector<int16_t> vecInt16 = { 1, 2, 3, -4, 5, -6 }; + std::vector<int32_t> vecInt32 = { 1, 2, 3, -4, 5, -6 }; + std::vector<int64_t> vecInt64 = { 1, 2, 3, -4, 5, -6 }; + std::vector<float> vecFloat = { 1.1, 2.2, 3.3, -4.4, 5.5, -6.6 }; + std::vector<double> vecDouble = { 1.1, 2.2, 3.3, -4.4, 5.5, -6.6 }; + std::vector<bool> vecBool = { true, true, false, false, true, false }; + std::vector<std::string> vecString = { "foo", "bar", "baz" }; + std::vector<ControlInfoMap> vecControlInfoMap = { + camera_->controls(), + Controls, + }; + + std::vector<uint8_t> buf; + std::vector<SharedFD> fds; + + if (testVectorSerdes(vecUint8) != TestPass) + return TestFail; + + if (testVectorSerdes(vecUint16) != TestPass) + return TestFail; + + if (testVectorSerdes(vecUint32) != TestPass) + return TestFail; + + if (testVectorSerdes(vecUint64) != TestPass) + return TestFail; + + if (testVectorSerdes(vecInt8) != TestPass) + return TestFail; + + if (testVectorSerdes(vecInt16) != TestPass) + return TestFail; + + if (testVectorSerdes(vecInt32) != TestPass) + return TestFail; + + if (testVectorSerdes(vecInt64) != TestPass) + return TestFail; + + if (testVectorSerdes(vecFloat) != TestPass) + return TestFail; + + if (testVectorSerdes(vecDouble) != TestPass) + return TestFail; + + if (testVectorSerdes(vecBool) != TestPass) + return TestFail; + + if (testVectorSerdes(vecString) != TestPass) + return TestFail; + + if (testVectorSerdes(vecControlInfoMap, &cs) != TestPass) + return TestFail; + + return TestPass; + } + + int testMap() + { + ControlSerializer cs(ControlSerializer::Role::Proxy); + + /* + * Realistically, only string and integral keys. + * Test simple, complex, and nested compound value. + */ + std::map<uint64_t, std::string> mapUintStr = + { { 101, "foo" }, { 102, "bar" }, { 103, "baz" } }; + std::map<int64_t, std::string> mapIntStr = + { { 101, "foo" }, { -102, "bar" }, { -103, "baz" } }; + std::map<std::string, std::string> mapStrStr = + { { "a", "foo" }, { "b", "bar" }, { "c", "baz" } }; + std::map<uint64_t, ControlInfoMap> mapUintCIM = + { { 201, camera_->controls() }, { 202, Controls } }; + std::map<int64_t, ControlInfoMap> mapIntCIM = + { { 201, camera_->controls() }, { -202, Controls } }; + std::map<std::string, ControlInfoMap> mapStrCIM = + { { "a", camera_->controls() }, { "b", Controls } }; + std::map<uint64_t, std::vector<uint8_t>> mapUintBVec = + { { 301, { 1, 2, 3 } }, { 302, { 4, 5, 6 } }, { 303, { 7, 8, 9 } } }; + std::map<int64_t, std::vector<uint8_t>> mapIntBVec = + { { 301, { 1, 2, 3 } }, { -302, { 4, 5, 6} }, { -303, { 7, 8, 9 } } }; + std::map<std::string, std::vector<uint8_t>> mapStrBVec = + { { "a", { 1, 2, 3 } }, { "b", { 4, 5, 6 } }, { "c", { 7, 8, 9 } } }; + + std::vector<uint8_t> buf; + std::vector<SharedFD> fds; + + if (testMapSerdes(mapUintStr) != TestPass) + return TestFail; + + if (testMapSerdes(mapIntStr) != TestPass) + return TestFail; + + if (testMapSerdes(mapStrStr) != TestPass) + return TestFail; + + if (testMapSerdes(mapUintCIM, &cs) != TestPass) + return TestFail; + + if (testMapSerdes(mapIntCIM, &cs) != TestPass) + return TestFail; + + if (testMapSerdes(mapStrCIM, &cs) != TestPass) + return TestFail; + + if (testMapSerdes(mapUintBVec) != TestPass) + return TestFail; + + if (testMapSerdes(mapIntBVec) != TestPass) + return TestFail; + + if (testMapSerdes(mapStrBVec) != TestPass) + return TestFail; + + return TestPass; + } + + int testPod() + { + uint32_t u32min = std::numeric_limits<uint32_t>::min(); + uint32_t u32max = std::numeric_limits<uint32_t>::max(); + uint32_t u32one = 1; + int32_t i32min = std::numeric_limits<int32_t>::min(); + int32_t i32max = std::numeric_limits<int32_t>::max(); + int32_t i32one = 1; + + uint64_t u64min = std::numeric_limits<uint64_t>::min(); + uint64_t u64max = std::numeric_limits<uint64_t>::max(); + uint64_t u64one = 1; + int64_t i64min = std::numeric_limits<int64_t>::min(); + int64_t i64max = std::numeric_limits<int64_t>::max(); + int64_t i64one = 1; + + float flow = std::numeric_limits<float>::lowest(); + float fmin = std::numeric_limits<float>::min(); + float fmax = std::numeric_limits<float>::max(); + float falmostOne = 1 + 1.0e-37; + double dlow = std::numeric_limits<double>::lowest(); + double dmin = std::numeric_limits<double>::min(); + double dmax = std::numeric_limits<double>::max(); + double dalmostOne = 1 + 1.0e-307; + + bool t = true; + bool f = false; + + std::stringstream ss; + for (unsigned int i = 0; i < (1 << 11); i++) + ss << "0123456789"; + + std::string strLong = ss.str(); + std::string strEmpty = ""; + + std::vector<uint8_t> buf; + std::vector<SharedFD> fds; + + if (testPodSerdes(u32min) != TestPass) + return TestFail; + + if (testPodSerdes(u32max) != TestPass) + return TestFail; + + if (testPodSerdes(u32one) != TestPass) + return TestFail; + + if (testPodSerdes(i32min) != TestPass) + return TestFail; + + if (testPodSerdes(i32max) != TestPass) + return TestFail; + + if (testPodSerdes(i32one) != TestPass) + return TestFail; + + if (testPodSerdes(u64min) != TestPass) + return TestFail; + + if (testPodSerdes(u64max) != TestPass) + return TestFail; + + if (testPodSerdes(u64one) != TestPass) + return TestFail; + + if (testPodSerdes(i64min) != TestPass) + return TestFail; + + if (testPodSerdes(i64max) != TestPass) + return TestFail; + + if (testPodSerdes(i64one) != TestPass) + return TestFail; + + if (testPodSerdes(flow) != TestPass) + return TestFail; + + if (testPodSerdes(fmin) != TestPass) + return TestFail; + + if (testPodSerdes(fmax) != TestPass) + return TestFail; + + if (testPodSerdes(falmostOne) != TestPass) + return TestFail; + + if (testPodSerdes(dlow) != TestPass) + return TestFail; + + if (testPodSerdes(dmin) != TestPass) + return TestFail; + + if (testPodSerdes(dmax) != TestPass) + return TestFail; + + if (testPodSerdes(dalmostOne) != TestPass) + return TestFail; + + if (testPodSerdes(t) != TestPass) + return TestFail; + + if (testPodSerdes(f) != TestPass) + return TestFail; + + if (testPodSerdes(strLong) != TestPass) + return TestFail; + + if (testPodSerdes(strEmpty) != TestPass) + return TestFail; + + return TestPass; + } +}; + +TEST_REGISTER(IPADataSerializerTest) diff --git a/test/serialization/meson.build b/test/serialization/meson.build index d78d92e6..a6e8d793 100644 --- a/test/serialization/meson.build +++ b/test/serialization/meson.build @@ -1,11 +1,16 @@ +# SPDX-License-Identifier: CC0-1.0 + +subdir('generated_serializer') + serialization_tests = [ - [ 'control_serialization', 'control_serialization.cpp' ], + {'name': 'control_serialization', 'sources': ['control_serialization.cpp']}, + {'name': 'ipa_data_serializer_test', 'sources': ['ipa_data_serializer_test.cpp']}, ] -foreach t : serialization_tests - exe = executable(t[0], [t[1], 'serialization_test.cpp'], - dependencies : libcamera_dep, +foreach test : serialization_tests + exe = executable(test['name'], test['sources'], 'serialization_test.cpp', + dependencies : libcamera_private, link_with : test_libraries, include_directories : test_includes_internal) - test(t[0], exe, suite : 'serialization', is_parallel : true) + test(test['name'], exe, suite : 'serialization', is_parallel : false) endforeach diff --git a/test/serialization/serialization_test.cpp b/test/serialization/serialization_test.cpp index 11d0f0f3..af9969fd 100644 --- a/test/serialization/serialization_test.cpp +++ b/test/serialization/serialization_test.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * serialization_test.cpp - Base class for serialization tests + * Base class for serialization tests */ #include "serialization_test.h" diff --git a/test/serialization/serialization_test.h b/test/serialization/serialization_test.h index fe77221e..760e3721 100644 --- a/test/serialization/serialization_test.h +++ b/test/serialization/serialization_test.h @@ -2,10 +2,10 @@ /* * Copyright (C) 2019, Google Inc. * - * serialization_test.h - Base class for serialization tests + * Base class for serialization tests */ -#ifndef __LIBCAMERA_SERIALIZATION_TEST_H__ -#define __LIBCAMERA_SERIALIZATION_TEST_H__ + +#pragma once #include <libcamera/camera.h> #include <libcamera/camera_manager.h> @@ -14,20 +14,16 @@ #include "camera_test.h" #include "test.h" -using namespace libcamera; - class SerializationTest : public CameraTest, public Test { public: SerializationTest() - : CameraTest("VIMC Sensor B") + : CameraTest("platform/vimc.0 Sensor B") { } - static bool equals(const ControlInfoMap &lhs, - const ControlInfoMap &rhs); - static bool equals(const ControlList &lhs, - const ControlList &rhs); + static bool equals(const libcamera::ControlInfoMap &lhs, + const libcamera::ControlInfoMap &rhs); + static bool equals(const libcamera::ControlList &lhs, + const libcamera::ControlList &rhs); }; - -#endif /* __LIBCAMERA_SERIALIZATION_TEST_H__ */ diff --git a/test/file-descriptor.cpp b/test/shared-fd.cpp index e467f3a7..57199dfe 100644 --- a/test/file-descriptor.cpp +++ b/test/shared-fd.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * file_descriptor.cpp - FileDescriptor test + * SharedFD test */ #include <fcntl.h> @@ -11,15 +11,15 @@ #include <sys/types.h> #include <unistd.h> -#include <libcamera/file_descriptor.h> +#include <libcamera/base/shared_fd.h> +#include <libcamera/base/utils.h> #include "test.h" -#include "utils.h" using namespace libcamera; using namespace std; -class FileDescriptorTest : public Test +class SharedFDTest : public Test { protected: int init() @@ -43,10 +43,10 @@ protected: int run() { - /* Test creating empty FileDescriptor. */ - desc1_ = new FileDescriptor(); + /* Test creating empty SharedFD. */ + desc1_ = new SharedFD(); - if (desc1_->fd() != -1) { + if (desc1_->get() != -1) { std::cout << "Failed fd numerical check (default constructor)" << std::endl; return TestFail; @@ -55,42 +55,77 @@ protected: delete desc1_; desc1_ = nullptr; - /* Test creating FileDescriptor from numerical file descriptor. */ - desc1_ = new FileDescriptor(fd_); - if (desc1_->fd() == fd_) { - std::cout << "Failed fd numerical check (int constructor)" + /* + * Test creating SharedFD by copying numerical file + * descriptor. + */ + desc1_ = new SharedFD(fd_); + if (desc1_->get() == fd_) { + std::cout << "Failed fd numerical check (lvalue ref constructor)" << std::endl; return TestFail; } - if (!isValidFd(fd_) || !isValidFd(desc1_->fd())) { - std::cout << "Failed fd validity after construction (int constructor)" + if (!isValidFd(fd_) || !isValidFd(desc1_->get())) { + std::cout << "Failed fd validity after construction (lvalue ref constructor)" << std::endl; return TestFail; } - int fd = desc1_->fd(); + int fd = desc1_->get(); delete desc1_; desc1_ = nullptr; if (!isValidFd(fd_) || isValidFd(fd)) { - std::cout << "Failed fd validity after destruction (int constructor)" + std::cout << "Failed fd validity after destruction (lvalue ref constructor)" << std::endl; return TestFail; } - /* Test creating FileDescriptor from other FileDescriptor. */ - desc1_ = new FileDescriptor(fd_); - desc2_ = new FileDescriptor(*desc1_); + /* + * Test creating SharedFD by taking ownership of + * numerical file descriptor. + */ + int dupFd = dup(fd_); + int dupFdCopy = dupFd; - if (desc1_->fd() == fd_ || desc2_->fd() == fd_ || desc1_->fd() != desc2_->fd()) { + desc1_ = new SharedFD(std::move(dupFd)); + if (desc1_->get() != dupFdCopy) { + std::cout << "Failed fd numerical check (rvalue ref constructor)" + << std::endl; + return TestFail; + } + + if (dupFd != -1 || !isValidFd(fd_) || !isValidFd(desc1_->get())) { + std::cout << "Failed fd validity after construction (rvalue ref constructor)" + << std::endl; + return TestFail; + } + + fd = desc1_->get(); + + delete desc1_; + desc1_ = nullptr; + + if (!isValidFd(fd_) || isValidFd(fd)) { + std::cout << "Failed fd validity after destruction (rvalue ref constructor)" + << std::endl; + return TestFail; + } + + /* Test creating SharedFD from other SharedFD. */ + desc1_ = new SharedFD(fd_); + desc2_ = new SharedFD(*desc1_); + + if (desc1_->get() == fd_ || desc2_->get() == fd_ || + desc1_->get() != desc2_->get()) { std::cout << "Failed fd numerical check (copy constructor)" << std::endl; return TestFail; } - if (!isValidFd(desc1_->fd()) || !isValidFd(desc2_->fd())) { + if (!isValidFd(desc1_->get()) || !isValidFd(desc2_->get())) { std::cout << "Failed fd validity after construction (copy constructor)" << std::endl; return TestFail; @@ -99,7 +134,7 @@ protected: delete desc1_; desc1_ = nullptr; - if (!isValidFd(desc2_->fd())) { + if (!isValidFd(desc2_->get())) { std::cout << "Failed fd validity after destruction (copy constructor)" << std::endl; return TestFail; @@ -108,18 +143,18 @@ protected: delete desc2_; desc2_ = nullptr; - /* Test creating FileDescriptor by taking over other FileDescriptor. */ - desc1_ = new FileDescriptor(fd_); - fd = desc1_->fd(); - desc2_ = new FileDescriptor(std::move(*desc1_)); + /* Test creating SharedFD by taking over other SharedFD. */ + desc1_ = new SharedFD(fd_); + fd = desc1_->get(); + desc2_ = new SharedFD(std::move(*desc1_)); - if (desc1_->fd() != -1 || desc2_->fd() != fd) { + if (desc1_->get() != -1 || desc2_->get() != fd) { std::cout << "Failed fd numerical check (move constructor)" << std::endl; return TestFail; } - if (!isValidFd(desc2_->fd())) { + if (!isValidFd(desc2_->get())) { std::cout << "Failed fd validity after construction (move constructor)" << std::endl; return TestFail; @@ -130,20 +165,20 @@ protected: delete desc2_; desc2_ = nullptr; - /* Test creating FileDescriptor by copy assignment. */ - desc1_ = new FileDescriptor(); - desc2_ = new FileDescriptor(fd_); + /* Test creating SharedFD by copy assignment. */ + desc1_ = new SharedFD(); + desc2_ = new SharedFD(fd_); - fd = desc2_->fd(); + fd = desc2_->get(); *desc1_ = *desc2_; - if (desc1_->fd() != fd || desc2_->fd() != fd) { + if (desc1_->get() != fd || desc2_->get() != fd) { std::cout << "Failed fd numerical check (copy assignment)" << std::endl; return TestFail; } - if (!isValidFd(desc1_->fd()) || !isValidFd(desc2_->fd())) { + if (!isValidFd(desc1_->get()) || !isValidFd(desc2_->get())) { std::cout << "Failed fd validity after construction (copy assignment)" << std::endl; return TestFail; @@ -154,20 +189,20 @@ protected: delete desc2_; desc2_ = nullptr; - /* Test creating FileDescriptor by move assignment. */ - desc1_ = new FileDescriptor(); - desc2_ = new FileDescriptor(fd_); + /* Test creating SharedFD by move assignment. */ + desc1_ = new SharedFD(); + desc2_ = new SharedFD(fd_); - fd = desc2_->fd(); + fd = desc2_->get(); *desc1_ = std::move(*desc2_); - if (desc1_->fd() != fd || desc2_->fd() != -1) { + if (desc1_->get() != fd || desc2_->get() != -1) { std::cout << "Failed fd numerical check (move assignment)" << std::endl; return TestFail; } - if (!isValidFd(desc1_->fd())) { + if (!isValidFd(desc1_->get())) { std::cout << "Failed fd validity after construction (move assignment)" << std::endl; return TestFail; @@ -203,7 +238,7 @@ private: int fd_; ino_t inodeNr_; - FileDescriptor *desc1_, *desc2_; + SharedFD *desc1_, *desc2_; }; -TEST_REGISTER(FileDescriptorTest) +TEST_REGISTER(SharedFDTest) diff --git a/test/signal-threads.cpp b/test/signal-threads.cpp index f77733eb..c4789c83 100644 --- a/test/signal-threads.cpp +++ b/test/signal-threads.cpp @@ -2,17 +2,19 @@ /* * Copyright (C) 2019, Google Inc. * - * signal-threads.cpp - Cross-thread signal delivery test + * Cross-thread signal delivery test */ #include <chrono> #include <iostream> #include <thread> -#include "message.h" -#include "thread.h" +#include <libcamera/base/message.h> +#include <libcamera/base/object.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/utils.h> + #include "test.h" -#include "utils.h" using namespace std; using namespace libcamera; @@ -57,15 +59,20 @@ private: class SignalThreadsTest : public Test { protected: - int run() + int init() { - SignalReceiver receiver; - signal_.connect(&receiver, &SignalReceiver::slot); + receiver_ = new SignalReceiver(); + signal_.connect(receiver_, &SignalReceiver::slot); + return TestPass; + } + + int run() + { /* Test that a signal is received in the main thread. */ signal_.emit(0); - switch (receiver.status()) { + switch (receiver_->status()) { case SignalReceiver::NoSignal: cout << "No signal received for direct connection" << endl; return TestFail; @@ -81,8 +88,8 @@ protected: * Move the object to a thread and verify that the signal is * correctly delivered, with the correct data. */ - receiver.reset(); - receiver.moveToThread(&thread_); + receiver_->reset(); + receiver_->moveToThread(&thread_); thread_.start(); @@ -90,7 +97,7 @@ protected: this_thread::sleep_for(chrono::milliseconds(100)); - switch (receiver.status()) { + switch (receiver_->status()) { case SignalReceiver::NoSignal: cout << "No signal received for message connection" << endl; return TestFail; @@ -102,7 +109,7 @@ protected: break; } - if (receiver.value() != 42) { + if (receiver_->value() != 42) { cout << "Signal received with incorrect value" << endl; return TestFail; } @@ -112,11 +119,13 @@ protected: void cleanup() { + receiver_->deleteLater(); thread_.exit(0); thread_.wait(); } private: + SignalReceiver *receiver_; Thread thread_; Signal<int> signal_; diff --git a/test/signal.cpp b/test/signal.cpp index f83ceb05..3f596b22 100644 --- a/test/signal.cpp +++ b/test/signal.cpp @@ -2,14 +2,14 @@ /* * Copyright (C) 2019, Google Inc. * - * signal.cpp - Signal test + * Signal test */ #include <iostream> #include <string.h> -#include <libcamera/object.h> -#include <libcamera/signal.h> +#include <libcamera/base/object.h> +#include <libcamera/base/signal.h> #include "test.h" @@ -41,8 +41,8 @@ class BaseClass { public: /* - * A virtual method is required in the base class, otherwise the compiler - * will always store Object before BaseClass in memory. + * A virtual function is required in the base class, otherwise the + * compiler will always store Object before BaseClass in memory. */ virtual ~BaseClass() { @@ -191,20 +191,40 @@ protected: signalVoid_.connect(slotStaticReturn); signalVoid_.connect(this, &SignalTest::slotReturn); + /* Test signal connection to a lambda. */ + int value = 0; + signalInt_.connect(this, [&](int v) { value = v; }); + signalInt_.emit(42); + + if (value != 42) { + cout << "Signal connection to lambda failed" << endl; + return TestFail; + } + + signalInt_.disconnect(this); + signalInt_.emit(0); + + if (value != 42) { + cout << "Signal disconnection from lambda failed" << endl; + return TestFail; + } + /* ----------------- Signal -> Object tests ----------------- */ /* - * Test automatic disconnection on object deletion. Connect the - * slot twice to ensure all instances are disconnected. + * Test automatic disconnection on object deletion. Connect two + * signals to ensure all instances are disconnected. */ signalVoid_.disconnect(); + signalVoid2_.disconnect(); SlotObject *slotObject = new SlotObject(); signalVoid_.connect(slotObject, &SlotObject::slot); - signalVoid_.connect(slotObject, &SlotObject::slot); + signalVoid2_.connect(slotObject, &SlotObject::slot); delete slotObject; valueStatic_ = 0; signalVoid_.emit(); + signalVoid2_.emit(); if (valueStatic_ != 0) { cout << "Signal disconnection on object deletion test failed" << endl; return TestFail; @@ -256,20 +276,43 @@ protected: delete slotObject; + /* Test signal connection to a lambda. */ + slotObject = new SlotObject(); + value = 0; + signalInt_.connect(slotObject, [&](int v) { value = v; }); + signalInt_.emit(42); + + if (value != 42) { + cout << "Signal connection to Object lambda failed" << endl; + return TestFail; + } + + signalInt_.disconnect(slotObject); + signalInt_.emit(0); + + if (value != 42) { + cout << "Signal disconnection from Object lambda failed" << endl; + return TestFail; + } + + delete slotObject; + /* --------- Signal -> Object (multiple inheritance) -------- */ /* - * Test automatic disconnection on object deletion. Connect the - * slot twice to ensure all instances are disconnected. + * Test automatic disconnection on object deletion. Connect two + * signals to ensure all instances are disconnected. */ signalVoid_.disconnect(); + signalVoid2_.disconnect(); SlotMulti *slotMulti = new SlotMulti(); signalVoid_.connect(slotMulti, &SlotMulti::slot); - signalVoid_.connect(slotMulti, &SlotMulti::slot); + signalVoid2_.connect(slotMulti, &SlotMulti::slot); delete slotMulti; valueStatic_ = 0; signalVoid_.emit(); + signalVoid2_.emit(); if (valueStatic_ != 0) { cout << "Signal disconnection on object deletion test failed" << endl; return TestFail; @@ -306,6 +349,7 @@ protected: private: Signal<> signalVoid_; + Signal<> signalVoid2_; Signal<int> signalInt_; Signal<int, const std::string &> signalMultiArgs_; diff --git a/test/span.cpp b/test/span.cpp index 69f0732e..4b9f3279 100644 --- a/test/span.cpp +++ b/test/span.cpp @@ -2,14 +2,14 @@ /* * Copyright (C) 2020, Google Inc. * - * span.cpp - Span tests + * Span tests */ /* * Include first to ensure the header is self-contained, as there's no span.cpp * in libcamera. */ -#include <libcamera/span.h> +#include <libcamera/base/span.h> #include <array> #include <iostream> @@ -72,12 +72,24 @@ protected: staticSpan = Span<int, 4>{ v }; - staticSpan.begin(); - staticSpan.cbegin(); + if (*staticSpan.begin() != 1) { + std::cout << "Span<static_extent>::begin() failed" << std::endl; + return TestFail; + } + if (*staticSpan.cbegin() != 1) { + std::cout << "Span<static_extent>::cbegin() failed" << std::endl; + return TestFail; + } staticSpan.end(); staticSpan.cend(); - staticSpan.rbegin(); - staticSpan.crbegin(); + if (*staticSpan.rbegin() != 4) { + std::cout << "Span<static_extent>::rbegin() failed" << std::endl; + return TestFail; + } + if (*staticSpan.crbegin() != 4) { + std::cout << "Span<static_extent>::crbegin() failed" << std::endl; + return TestFail; + } staticSpan.rend(); staticSpan.crend(); @@ -106,7 +118,7 @@ protected: /* staticSpan.subspan(2, 4); */ /* - * Compile-test construction and usage of spans with static + * Compile-test construction and usage of spans with dynamic * extent. Commented-out tests are expected not to compile, or * to generate undefined behaviour. */ @@ -131,9 +143,9 @@ protected: Span<const int>{ v }; /* Span<float>{ v }; */ - Span<const int>{ v }; - /* Span<int>{ v }; */ - /* Span<const float>{ v }; */ + Span<const int>{ cv }; + /* Span<int>{ cv }; */ + /* Span<const float>{ cv }; */ Span<int> dynamicSpan{ i }; Span<int>{ dynamicSpan }; @@ -141,12 +153,24 @@ protected: dynamicSpan = Span<int>{ a }; - dynamicSpan.begin(); - dynamicSpan.cbegin(); + if (*dynamicSpan.begin() != 1) { + std::cout << "Span<dynamic_extent>::begin() failed" << std::endl; + return TestFail; + } + if (*dynamicSpan.cbegin() != 1) { + std::cout << "Span<dynamic_extent>::cbegin() failed" << std::endl; + return TestFail; + } dynamicSpan.end(); dynamicSpan.cend(); - dynamicSpan.rbegin(); - dynamicSpan.crbegin(); + if (*dynamicSpan.rbegin() != 4) { + std::cout << "Span<dynamic_extent>::rbegin() failed" << std::endl; + return TestFail; + } + if (*dynamicSpan.crbegin() != 4) { + std::cout << "Span<dynamic_extent>::crbegin() failed" << std::endl; + return TestFail; + } dynamicSpan.rend(); dynamicSpan.crend(); diff --git a/test/stream/meson.build b/test/stream/meson.build index 005f4aa4..dd77f2f7 100644 --- a/test/stream/meson.build +++ b/test/stream/meson.build @@ -1,11 +1,14 @@ +# SPDX-License-Identifier: CC0-1.0 + stream_tests = [ - [ 'stream_formats', 'stream_formats.cpp' ], + {'name': 'stream_colorspace', 'sources': ['stream_colorspace.cpp']}, + {'name': 'stream_formats', 'sources': ['stream_formats.cpp']}, ] -foreach t : stream_tests - exe = executable(t[0], t[1], - dependencies : libcamera_dep, - link_with : test_libraries, - include_directories : test_includes_internal) - test(t[0], exe, suite: 'stream') +foreach test : stream_tests + exe = executable(test['name'], test['sources'], + dependencies : libcamera_public, + link_with : test_libraries, + include_directories : test_includes_internal) + test(test['name'], exe, suite : 'stream') endforeach diff --git a/test/stream/stream_colorspace.cpp b/test/stream/stream_colorspace.cpp new file mode 100644 index 00000000..4c904c4c --- /dev/null +++ b/test/stream/stream_colorspace.cpp @@ -0,0 +1,96 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy. + * + * Stream colorspace adjustment test + */ + +#include <iostream> + +#include <libcamera/camera.h> +#include <libcamera/formats.h> +#include <libcamera/stream.h> + +#include "test.h" + +using namespace libcamera; +using namespace std; + +class TestCameraConfiguration : public CameraConfiguration +{ +public: + TestCameraConfiguration() + : CameraConfiguration() + { + } + + Status validate() override + { + return validateColorSpaces(); + } +}; + +class StreamColorSpaceTest : public Test +{ +protected: + int run() + { + TestCameraConfiguration config; + + StreamConfiguration cfg; + cfg.size = { 640, 320 }; + cfg.pixelFormat = formats::YUV422; + cfg.colorSpace = ColorSpace::Srgb; + config.addConfiguration(cfg); + + StreamConfiguration &streamCfg = config.at(0); + + /* + * YUV pixelformat with sRGB colorspace should have Y'CbCr encoding + * adjusted. + */ + config.validate(); + if (streamCfg.colorSpace->ycbcrEncoding == ColorSpace::YcbcrEncoding::None) { + cerr << "YUV format must have YCbCr encoding" << endl; + return TestFail; + } + + /* + * For YUV pixelFormat, encoding should be picked up according + * to primaries and transfer function, if 'None' is specified. + */ + streamCfg.pixelFormat = formats::YUV422; + streamCfg.colorSpace = ColorSpace(ColorSpace::Primaries::Rec2020, + ColorSpace::TransferFunction::Rec709, + ColorSpace::YcbcrEncoding::None, + ColorSpace::Range::Limited); + config.validate(); + if (streamCfg.colorSpace->ycbcrEncoding != ColorSpace::YcbcrEncoding::Rec2020) { + cerr << "Failed to adjust colorspace Y'CbCr encoding according" + << " to primaries and transfer function" << endl; + return TestFail; + } + + /* For RGB pixelFormat, Sycc colorspace should get adjusted to sRGB. */ + streamCfg.pixelFormat = formats::RGB888; + streamCfg.colorSpace = ColorSpace::Sycc; + config.validate(); + if (streamCfg.colorSpace != ColorSpace::Srgb) { + cerr << "RGB format's colorspace should be set to Srgb" << endl; + return TestFail; + } + + /* Raw formats should always set colorspace to ColorSpace::Raw. */ + streamCfg.pixelFormat = formats::SBGGR8; + streamCfg.colorSpace = ColorSpace::Rec709; + config.validate(); + if (streamCfg.colorSpace != ColorSpace::Raw) { + cerr << "Raw format must always have Raw colorspace" << endl; + return TestFail; + } + + return TestPass; + } +}; + +TEST_REGISTER(StreamColorSpaceTest) diff --git a/test/stream/stream_formats.cpp b/test/stream/stream_formats.cpp index 9353d008..553b59aa 100644 --- a/test/stream/stream_formats.cpp +++ b/test/stream/stream_formats.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * stream_formats.cpp - StreamFormats test + * StreamFormats test */ #include <iostream> @@ -40,10 +40,10 @@ protected: cout << "Failed " << name << endl; cout << "Sizes to test:" << endl; for (Size &size : test) - cout << size.toString() << endl; + cout << size << endl; cout << "Valid sizes:" << endl; for (Size &size : valid) - cout << size.toString() << endl; + cout << size << endl; return TestFail; } diff --git a/test/threads.cpp b/test/threads.cpp index 0454761d..8d6ee151 100644 --- a/test/threads.cpp +++ b/test/threads.cpp @@ -2,14 +2,20 @@ /* * Copyright (C) 2019, Google Inc. * - * threads.cpp - Threads test + * Threads test */ #include <chrono> #include <iostream> +#include <memory> +#include <pthread.h> +#include <sched.h> #include <thread> +#include <time.h> + +#include <libcamera/base/object.h> +#include <libcamera/base/thread.h> -#include "thread.h" #include "test.h" using namespace std; @@ -33,6 +39,56 @@ private: chrono::steady_clock::duration duration_; }; +class CancelThread : public Thread +{ +public: + CancelThread(bool &cancelled) + : cancelled_(cancelled) + { + } + +protected: + void run() + { + cancelled_ = true; + + /* + * Cancel the thread and call a guaranteed cancellation point + * (nanosleep). + */ + pthread_cancel(pthread_self()); + + struct timespec req{ 0, 100*000*000 }; + nanosleep(&req, nullptr); + + cancelled_ = false; + } + +private: + bool &cancelled_; +}; + +class CpuSetTester : public Object +{ +public: + CpuSetTester(unsigned int cpuset) + : cpuset_(cpuset) {} + + bool testCpuSet() + { + int ret = sched_getcpu(); + if (static_cast<int>(cpuset_) != ret) { + cout << "Invalid cpuset: " << ret << ", expecting: " << cpuset_ << endl; + return false; + } + + return true; + } + +private: + const unsigned int cpuset_; +}; + class ThreadTest : public Test { protected: @@ -44,23 +100,23 @@ protected: int run() { /* Test Thread() retrieval for the main thread. */ - Thread *thread = Thread::current(); - if (!thread) { + Thread *mainThread = Thread::current(); + if (!mainThread) { cout << "Thread::current() failed to main thread" << endl; return TestFail; } - if (!thread->isRunning()) { + if (!mainThread->isRunning()) { cout << "Main thread is not running" << endl; return TestFail; } /* Test starting the main thread, the test shall not crash. */ - thread->start(); + mainThread->start(); /* Test the running state of a custom thread. */ - thread = new Thread(); + std::unique_ptr<Thread> thread = std::make_unique<Thread>(); thread->start(); if (!thread->isRunning()) { @@ -78,10 +134,8 @@ protected: return TestFail; } - delete thread; - /* Test waiting for completion with a timeout. */ - thread = new DelayThread(chrono::milliseconds(500)); + thread = std::make_unique<DelayThread>(chrono::milliseconds(500)); thread->start(); thread->exit(0); @@ -99,10 +153,8 @@ protected: return TestFail; } - delete thread; - /* Test waiting on a thread that isn't running. */ - thread = new Thread(); + thread = std::make_unique<Thread>(); timeout = !thread->wait(); if (timeout) { @@ -120,6 +172,39 @@ protected: return TestFail; } + /* Test thread cleanup upon abnormal termination. */ + bool cancelled = false; + bool finished = false; + + thread = std::make_unique<CancelThread>(cancelled); + thread->finished.connect(this, [&finished]() { finished = true; }); + + thread->start(); + thread->exit(0); + thread->wait(chrono::milliseconds(1000)); + + if (!cancelled || !finished) { + cout << "Cleanup failed upon abnormal termination" << endl; + return TestFail; + } + + const unsigned int numCpus = std::thread::hardware_concurrency(); + for (unsigned int i = 0; i < numCpus; ++i) { + thread = std::make_unique<Thread>(); + const std::array<const unsigned int, 1> cpus{ i }; + thread->setThreadAffinity(cpus); + thread->start(); + + CpuSetTester tester(i); + tester.moveToThread(thread.get()); + + if (!tester.invokeMethod(&CpuSetTester::testCpuSet, ConnectionTypeBlocking)) + return TestFail; + + thread->exit(0); + thread->wait(); + } + return TestPass; } diff --git a/test/timer-fail.cpp b/test/timer-fail.cpp new file mode 100644 index 00000000..0ced6441 --- /dev/null +++ b/test/timer-fail.cpp @@ -0,0 +1,109 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2024, Ideas on Board Oy + * + * Threaded timer failure test + */ + +#include <chrono> +#include <iostream> + +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/object.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> + +#include "test.h" + +using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; + +class TimeoutHandler : public Object +{ +public: + TimeoutHandler() + : timer_(this), timeout_(false) + { + timer_.timeout.connect(this, &TimeoutHandler::timeoutHandler); + } + + void start() + { + timer_.start(100ms); + } + + bool timeout() const + { + return timeout_; + } + +private: + void timeoutHandler() + { + timeout_ = true; + } + + Timer timer_; + bool timeout_; +}; + +class TimerFailTest : public Test +{ +protected: + int init() + { + thread_.start(); + + timeout_ = new TimeoutHandler(); + timeout_->moveToThread(&thread_); + + return TestPass; + } + + int run() + { + /* + * Test that the forbidden operation of starting the timer from + * another thread results in a failure. We need to interrupt the + * event dispatcher to make sure we don't succeed simply because + * the event dispatcher hasn't noticed the timer restart. + */ + timeout_->start(); + thread_.eventDispatcher()->interrupt(); + + this_thread::sleep_for(chrono::milliseconds(200)); + + /* + * The wrong start() call should result in an assertion in debug + * builds, and a timeout in release builds. The test is + * therefore marked in meson.build as expected to fail. We need + * to return TestPass in the unexpected (usually known as + * "fail") case, and TestFail otherwise. + */ + if (timeout_->timeout()) { + cout << "Timer start from wrong thread succeeded unexpectedly" + << endl; + return TestPass; + } + + return TestFail; + } + + void cleanup() + { + /* + * Object class instances must be destroyed from the thread + * they live in. + */ + timeout_->deleteLater(); + thread_.exit(0); + thread_.wait(); + } + +private: + TimeoutHandler *timeout_; + Thread thread_; +}; + +TEST_REGISTER(TimerFailTest) diff --git a/test/timer-thread.cpp b/test/timer-thread.cpp index 32853b4e..55e5cfdf 100644 --- a/test/timer-thread.cpp +++ b/test/timer-thread.cpp @@ -2,20 +2,22 @@ /* * Copyright (C) 2019, Google Inc. * - * timer-thread.cpp - Threaded timer test + * Threaded timer test */ #include <chrono> #include <iostream> -#include <libcamera/event_dispatcher.h> -#include <libcamera/timer.h> +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/object.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> #include "test.h" -#include "thread.h" -using namespace std; using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; class TimeoutHandler : public Object { @@ -24,13 +26,7 @@ public: : timer_(this), timeout_(false) { timer_.timeout.connect(this, &TimeoutHandler::timeoutHandler); - timer_.start(100); - } - - void restart() - { - timeout_ = false; - timer_.start(100); + timer_.start(100ms); } bool timeout() const @@ -39,7 +35,7 @@ public: } private: - void timeoutHandler(Timer *timer) + void timeoutHandler() { timeout_ = true; } @@ -54,7 +50,9 @@ protected: int init() { thread_.start(); - timeout_.moveToThread(&thread_); + + timeout_ = new TimeoutHandler(); + timeout_->moveToThread(&thread_); return TestPass; } @@ -67,39 +65,27 @@ protected: */ this_thread::sleep_for(chrono::milliseconds(200)); - if (!timeout_.timeout()) { + if (!timeout_->timeout()) { cout << "Timer expiration test failed" << endl; return TestFail; } - /* - * Test that starting the timer from another thread fails. We - * need to interrupt the event dispatcher to make sure we don't - * succeed simply because the event dispatcher hasn't noticed - * the timer restart. - */ - timeout_.restart(); - thread_.eventDispatcher()->interrupt(); - - this_thread::sleep_for(chrono::milliseconds(200)); - - if (timeout_.timeout()) { - cout << "Timer restart test failed" << endl; - return TestFail; - } - return TestPass; } void cleanup() { - /* Must stop thread before destroying timeout. */ + /* + * Object class instances must be destroyed from the thread + * they live in. + */ + timeout_->deleteLater(); thread_.exit(0); thread_.wait(); } private: - TimeoutHandler timeout_; + TimeoutHandler *timeout_; Thread thread_; }; diff --git a/test/timer.cpp b/test/timer.cpp index 2bdb006e..2eacc059 100644 --- a/test/timer.cpp +++ b/test/timer.cpp @@ -2,20 +2,21 @@ /* * Copyright (C) 2019, Google Inc. * - * timer.cpp - Timer test + * Timer test */ #include <chrono> #include <iostream> -#include <libcamera/event_dispatcher.h> -#include <libcamera/timer.h> +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> #include "test.h" -#include "thread.h" -using namespace std; using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; class ManagedTimer : public Timer { @@ -26,7 +27,7 @@ public: timeout.connect(this, &ManagedTimer::timeoutHandler); } - void start(int msec) + void start(std::chrono::milliseconds msec) { count_ = 0; start_ = std::chrono::steady_clock::now(); @@ -56,7 +57,7 @@ public: } private: - void timeoutHandler(Timer *timer) + void timeoutHandler() { expiration_ = std::chrono::steady_clock::now(); count_++; @@ -82,7 +83,7 @@ protected: ManagedTimer timer2; /* Timer expiration. */ - timer.start(1000); + timer.start(1000ms); if (!timer.isRunning()) { cout << "Timer expiration test failed" << endl; @@ -101,7 +102,7 @@ protected: * Nanosecond resolution in a 32 bit value wraps at 4.294967 * seconds (0xFFFFFFFF / 1000000) */ - timer.start(4295); + timer.start(4295ms); dispatcher->processEvents(); if (timer.hasFailed()) { @@ -110,7 +111,7 @@ protected: } /* Timer restart. */ - timer.start(500); + timer.start(500ms); if (!timer.isRunning()) { cout << "Timer restart test failed" << endl; @@ -125,9 +126,9 @@ protected: } /* Timer restart before expiration. */ - timer.start(50); - timer.start(100); - timer.start(150); + timer.start(50ms); + timer.start(100ms); + timer.start(150ms); dispatcher->processEvents(); @@ -147,8 +148,8 @@ protected: } /* Two timers. */ - timer.start(1000); - timer2.start(300); + timer.start(1000ms); + timer2.start(300ms); dispatcher->processEvents(); @@ -170,8 +171,8 @@ protected: } /* Restart timer before expiration. */ - timer.start(1000); - timer2.start(300); + timer.start(1000ms); + timer2.start(300ms); dispatcher->processEvents(); @@ -180,7 +181,7 @@ protected: return TestFail; } - timer.start(1000); + timer.start(1000ms); dispatcher->processEvents(); @@ -194,10 +195,10 @@ protected: * deleted. This will result in a crash on failure. */ ManagedTimer *dyntimer = new ManagedTimer(); - dyntimer->start(100); + dyntimer->start(100ms); delete dyntimer; - timer.start(200); + timer.start(200ms); dispatcher->processEvents(); return TestPass; diff --git a/test/transform.cpp b/test/transform.cpp new file mode 100644 index 00000000..4ec7a1eb --- /dev/null +++ b/test/transform.cpp @@ -0,0 +1,329 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2023, Ideas On Board Oy + * + * Transform and Orientation tests + */ + +#include <iostream> + +#include <libcamera/orientation.h> +#include <libcamera/transform.h> + +#include "test.h" + +using namespace std; +using namespace libcamera; + +class TransformTest : public Test +{ +protected: + int run(); +}; + +int TransformTest::run() +{ + /* + * RotationTestEntry collects two Orientation and one Transform that + * gets combined to validate that (o1 / o2 = T) and (o1 = o2 * T) + * + * o1 / o2 = t computes the Transform to apply to o2 to obtain o1 + * o2 * t = o1 combines o2 with t by applying o2 first then t + * + * The comments on the (most complex) transform show how applying to + * an image with orientation o2 the Transform t allows to obtain o1. + * + * The image with basic rotation0 is assumed to be: + * + * AB + * CD + * + * And the Transform operators are: + * + * V = vertical flip + * H = horizontal flip + * T = transpose + * + * the operator '* (T|V)' applies V first then T. + */ + static const struct RotationTestEntry { + Orientation o1; + Orientation o2; + Transform t; + } testEntries[] = { + /* Test identities transforms first. */ + { + Orientation::Rotate0, Orientation::Rotate0, + Transform::Identity, + }, + { + Orientation::Rotate0Mirror, Orientation::Rotate0Mirror, + Transform::Identity, + }, + { + Orientation::Rotate180, Orientation::Rotate180, + Transform::Identity, + }, + { + Orientation::Rotate180Mirror, Orientation::Rotate180Mirror, + Transform::Identity, + }, + { + Orientation::Rotate90, Orientation::Rotate90, + Transform::Identity, + }, + { + Orientation::Rotate90Mirror, Orientation::Rotate90Mirror, + Transform::Identity, + }, + { + Orientation::Rotate270, Orientation::Rotate270, + Transform::Identity, + }, + { + Orientation::Rotate270Mirror, Orientation::Rotate270Mirror, + Transform::Identity, + }, + /* + * Combine 0 and 180 degrees rotation as they're the most common + * ones. + */ + { + /* + * o2 t o1 + * -------------------------- + * CD * (H|V) = BA AB + * BA CD CD + */ + Orientation::Rotate0, Orientation::Rotate180, + Transform::Rot180, + }, + { + /* + * o2 t o1 + * -------------------------- + * AB * (H|V) = CD DC + * CD AB BA + */ + Orientation::Rotate180, Orientation::Rotate0, + Transform::Rot180 + }, + /* Test that transpositions are handled correctly. */ + { + /* + * o2 t o1 + * -------------------------- + * AB * (T|V) = CD CA + * CD AB DB + */ + Orientation::Rotate90, Orientation::Rotate0, + Transform::Rot90, + }, + { + /* + * o2 t o1 + * -------------------------- + * CA * (T|H) = AC AB + * DB BD CD + */ + Orientation::Rotate0, Orientation::Rotate90, + Transform::Rot270, + }, + { + /* + * o2 t o1 + * -------------------------- + * AB * (T|H) = BA BD + * CD DC AC + */ + Orientation::Rotate270, Orientation::Rotate0, + Transform::Rot270, + }, + { + /* + * o2 t o1 + * -------------------------- + * BD * (T|V) = AC AB + * AC BD CD + */ + Orientation::Rotate0, Orientation::Rotate270, + Transform::Rot90, + }, + { + /* + * o2 t o1 + * -------------------------- + * CD * (T|H) = DC DA + * BA AB CB + */ + Orientation::Rotate90, Orientation::Rotate180, + Transform::Rot270, + }, + { + /* + * o2 t o1 + * -------------------------- + * DA * (T|V) = CB CD + * CB DA BA + */ + Orientation::Rotate180, Orientation::Rotate90, + Transform::Rot90, + }, + { + /* + * o2 t o1 + * -------------------------- + * CD * (T|V) = BA BC + * BA CD AD + */ + Orientation::Rotate270, Orientation::Rotate180, + Transform::Rot90, + }, + { + /* + * o2 t o1 + * -------------------------- + * BC * (T|H) = CB CD + * AD DA BA + */ + Orientation::Rotate180, Orientation::Rotate270, + Transform::Rot270, + }, + { + /* + * o2 t o1 + * -------------------------- + * DA * (V|H) = AD BC + * CB BC AD + */ + Orientation::Rotate270, Orientation::Rotate90, + Transform::Rot180, + }, + /* Test that mirroring is handled correctly. */ + { + Orientation::Rotate0, Orientation::Rotate0Mirror, + Transform::HFlip + }, + { + Orientation::Rotate0Mirror, Orientation::Rotate0, + Transform::HFlip + }, + { + Orientation::Rotate180, Orientation::Rotate180Mirror, + Transform::HFlip + }, + { + Orientation::Rotate180Mirror, Orientation::Rotate180, + Transform::HFlip + }, + { + Orientation::Rotate90, Orientation::Rotate90Mirror, + Transform::HFlip + }, + { + Orientation::Rotate90Mirror, Orientation::Rotate90, + Transform::HFlip + }, + { + Orientation::Rotate270, Orientation::Rotate270Mirror, + Transform::HFlip + }, + { + Orientation::Rotate270Mirror, Orientation::Rotate270, + Transform::HFlip + }, + { + Orientation::Rotate0, Orientation::Rotate0Mirror, + Transform::HFlip + }, + /* + * More exotic transforms which include Transpositions and + * mirroring. + */ + { + /* + * o2 t o1 + * ------------------ + * BC * (V) = AD + * AD BC + */ + Orientation::Rotate90Mirror, Orientation::Rotate270, + Transform::VFlip, + }, + { + /* + * o2 t o1 + * ------------------ + * CB * (T) = CD + * DA BA + */ + Orientation::Rotate180, Orientation::Rotate270Mirror, + Transform::Transpose, + }, + { + /* + * o2 t o1 + * ------------------ + * AD * (T) = AB + * BC DC + */ + Orientation::Rotate0, Orientation::Rotate90Mirror, + Transform::Transpose, + }, + { + /* + * o2 t o1 + * ------------------ + * AD * (V) = BC + * BC AD + */ + Orientation::Rotate270, Orientation::Rotate90Mirror, + Transform::VFlip, + }, + { + /* + * o2 t o1 + * ------------------ + * DA * (V) = CB + * CB DA + */ + Orientation::Rotate270Mirror, Orientation::Rotate90, + Transform::VFlip, + }, + { + /* + * o2 t o1 + * -------------------------- + * CB * (V|H) = BC AD + * DA AD BC + */ + Orientation::Rotate90Mirror, Orientation::Rotate270Mirror, + Transform::Rot180, + }, + }; + + for (const auto &entry : testEntries) { + Transform transform = entry.o1 / entry.o2; + if (transform != entry.t) { + cerr << "Failed to validate: " << entry.o1 + << " / " << entry.o2 + << " = " << transformToString(entry.t) << endl; + cerr << "Got back: " + << transformToString(transform) << endl; + return TestFail; + } + + Orientation adjusted = entry.o2 * entry.t; + if (adjusted != entry.o1) { + cerr << "Failed to validate: " << entry.o2 + << " * " << transformToString(entry.t) + << " = " << entry.o1 << endl; + cerr << "Got back: " << adjusted << endl; + return TestFail; + } + } + + return TestPass; +} + +TEST_REGISTER(TransformTest) diff --git a/test/unique-fd.cpp b/test/unique-fd.cpp new file mode 100644 index 00000000..e556439e --- /dev/null +++ b/test/unique-fd.cpp @@ -0,0 +1,220 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2021, Google Inc. + * + * UniqueFD test + */ + +#include <fcntl.h> +#include <iostream> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include <libcamera/base/unique_fd.h> +#include <libcamera/base/utils.h> + +#include "test.h" + +using namespace libcamera; +using namespace std; + +class UniqueFDTest : public Test +{ +protected: + int init() override + { + return createFd(); + } + + int run() override + { + /* Test creating empty UniqueFD. */ + UniqueFD fd; + + if (fd.isValid() || fd.get() != -1) { + std::cout << "Failed fd check (default constructor)" + << std::endl; + return TestFail; + } + + /* Test creating UniqueFD from numerical file descriptor. */ + UniqueFD fd2(fd_); + if (!fd2.isValid() || fd2.get() != fd_) { + std::cout << "Failed fd check (fd constructor)" + << std::endl; + return TestFail; + } + + if (!isValidFd(fd_)) { + std::cout << "Failed fd validity (fd constructor)" + << std::endl; + return TestFail; + } + + /* Test move constructor. */ + UniqueFD fd3(std::move(fd2)); + if (!fd3.isValid() || fd3.get() != fd_) { + std::cout << "Failed fd check (move constructor)" + << std::endl; + return TestFail; + } + + if (fd2.isValid() || fd2.get() != -1) { + std::cout << "Failed moved fd check (move constructor)" + << std::endl; + return TestFail; + } + + if (!isValidFd(fd_)) { + std::cout << "Failed fd validity (move constructor)" + << std::endl; + return TestFail; + } + + /* Test move assignment operator. */ + fd = std::move(fd3); + if (!fd.isValid() || fd.get() != fd_) { + std::cout << "Failed fd check (move assignment)" + << std::endl; + return TestFail; + } + + if (fd3.isValid() || fd3.get() != -1) { + std::cout << "Failed moved fd check (move assignment)" + << std::endl; + return TestFail; + } + + if (!isValidFd(fd_)) { + std::cout << "Failed fd validity (move assignment)" + << std::endl; + return TestFail; + } + + /* Test swapping. */ + fd2.swap(fd); + if (!fd2.isValid() || fd2.get() != fd_) { + std::cout << "Failed fd check (swap)" + << std::endl; + return TestFail; + } + + if (fd.isValid() || fd.get() != -1) { + std::cout << "Failed swapped fd check (swap)" + << std::endl; + return TestFail; + } + + if (!isValidFd(fd_)) { + std::cout << "Failed fd validity (swap)" + << std::endl; + return TestFail; + } + + /* Test release. */ + int numFd = fd2.release(); + if (fd2.isValid() || fd2.get() != -1) { + std::cout << "Failed fd check (release)" + << std::endl; + return TestFail; + } + + if (numFd != fd_) { + std::cout << "Failed released fd check (release)" + << std::endl; + return TestFail; + } + + if (!isValidFd(fd_)) { + std::cout << "Failed fd validity (release)" + << std::endl; + return TestFail; + } + + /* Test reset assignment. */ + fd.reset(numFd); + if (!fd.isValid() || fd.get() != fd_) { + std::cout << "Failed fd check (reset assignment)" + << std::endl; + return TestFail; + } + + if (!isValidFd(fd_)) { + std::cout << "Failed fd validity (reset assignment)" + << std::endl; + return TestFail; + } + + /* Test reset destruction. */ + fd.reset(); + if (fd.isValid() || fd.get() != -1) { + std::cout << "Failed fd check (reset destruction)" + << std::endl; + return TestFail; + } + + if (isValidFd(fd_)) { + std::cout << "Failed fd validity (reset destruction)" + << std::endl; + return TestFail; + } + + /* Test destruction. */ + if (createFd() == TestFail) { + std::cout << "Failed to recreate test fd" + << std::endl; + return TestFail; + } + + { + UniqueFD fd4(fd_); + } + + if (isValidFd(fd_)) { + std::cout << "Failed fd validity (destruction)" + << std::endl; + return TestFail; + } + + return TestPass; + } + + void cleanup() override + { + if (fd_ > 0) + close(fd_); + } + +private: + int createFd() + { + fd_ = open("/tmp", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR); + if (fd_ < 0) + return TestFail; + + /* Cache inode number of temp file. */ + struct stat s; + if (fstat(fd_, &s)) + return TestFail; + + inodeNr_ = s.st_ino; + + return 0; + } + + bool isValidFd(int fd) + { + struct stat s; + if (fstat(fd, &s)) + return false; + + /* Check that inode number matches cached temp file. */ + return s.st_ino == inodeNr_; + } + + int fd_; + ino_t inodeNr_; +}; + +TEST_REGISTER(UniqueFDTest) diff --git a/test/utils.cpp b/test/utils.cpp index 58816f15..d25475cb 100644 --- a/test/utils.cpp +++ b/test/utils.cpp @@ -2,19 +2,26 @@ /* * Copyright (C) 2018, Google Inc. * - * utils.cpp - Miscellaneous utility tests + * Miscellaneous utility tests */ #include <iostream> +#include <map> +#include <optional> #include <sstream> #include <string> #include <vector> +#include <libcamera/base/span.h> +#include <libcamera/base/utils.h> + +#include <libcamera/geometry.h> + #include "test.h" -#include "utils.h" using namespace std; using namespace libcamera; +using namespace std::literals::chrono_literals; class UtilsTest : public Test { @@ -69,12 +76,114 @@ protected: return TestPass; } + int testEnumerate() + { + std::vector<unsigned int> integers{ 1, 2, 3, 4, 5 }; + unsigned int i = 0; + + for (auto [index, value] : utils::enumerate(integers)) { + if (index != i || value != i + 1) { + cerr << "utils::enumerate(<vector>) test failed: i=" << i + << ", index=" << index << ", value=" << value + << std::endl; + return TestFail; + } + + /* Verify that we can modify the value. */ + --value; + ++i; + } + + if (integers != std::vector<unsigned int>{ 0, 1, 2, 3, 4 }) { + cerr << "Failed to modify container in enumerated range loop" << endl; + return TestFail; + } + + Span<const unsigned int> span{ integers }; + i = 0; + + for (auto [index, value] : utils::enumerate(span)) { + if (index != i || value != i) { + cerr << "utils::enumerate(<span>) test failed: i=" << i + << ", index=" << index << ", value=" << value + << std::endl; + return TestFail; + } + + ++i; + } + + const unsigned int array[] = { 0, 2, 4, 6, 8 }; + i = 0; + + for (auto [index, value] : utils::enumerate(array)) { + if (index != i || value != i * 2) { + cerr << "utils::enumerate(<array>) test failed: i=" << i + << ", index=" << index << ", value=" << value + << std::endl; + return TestFail; + } + + ++i; + } + + return TestPass; + } + + int testDuration() + { + std::ostringstream os; + utils::Duration exposure; + double ratio; + + exposure = 25ms + 25ms; + if (exposure.get<std::micro>() != 50000.0) { + cerr << "utils::Duration failed to return microsecond count"; + return TestFail; + } + + exposure = 1.0s / 4; + if (exposure != 250ms) { + cerr << "utils::Duration failed scalar divide test"; + return TestFail; + } + + exposure = 5000.5us; + if (!exposure) { + cerr << "utils::Duration failed boolean test"; + return TestFail; + } + + os << exposure; + if (os.str() != "5000.50us") { + cerr << "utils::Duration operator << failed"; + return TestFail; + } + + exposure = 100ms; + ratio = exposure / 25ms; + if (ratio != 4.0) { + cerr << "utils::Duration failed ratio test"; + return TestFail; + } + + return TestPass; + } + int run() { /* utils::hex() test. */ std::ostringstream os; std::string ref; + os << utils::hex(static_cast<int8_t>(0x42)) << " "; + ref += "0x42 "; + os << utils::hex(static_cast<uint8_t>(0x42)) << " "; + ref += "0x42 "; + os << utils::hex(static_cast<int16_t>(0x42)) << " "; + ref += "0x0042 "; + os << utils::hex(static_cast<uint16_t>(0x42)) << " "; + ref += "0x0042 "; os << utils::hex(static_cast<int32_t>(0x42)) << " "; ref += "0x00000042 "; os << utils::hex(static_cast<uint32_t>(0x42)) << " "; @@ -83,6 +192,15 @@ protected: ref += "0x0000000000000042 "; os << utils::hex(static_cast<uint64_t>(0x42)) << " "; ref += "0x0000000000000042 "; + + os << utils::hex(static_cast<int8_t>(0x42), 6) << " "; + ref += "0x000042 "; + os << utils::hex(static_cast<uint8_t>(0x42), 1) << " "; + ref += "0x42 "; + os << utils::hex(static_cast<int16_t>(0x42), 6) << " "; + ref += "0x000042 "; + os << utils::hex(static_cast<uint16_t>(0x42), 1) << " "; + ref += "0x42 "; os << utils::hex(static_cast<int32_t>(0x42), 4) << " "; ref += "0x0042 "; os << utils::hex(static_cast<uint32_t>(0x42), 1) << " "; @@ -99,7 +217,7 @@ protected: return TestFail; } - /* utils::split() test. */ + /* utils::join() and utils::split() test. */ std::vector<std::string> elements = { "/bin", "/usr/bin", @@ -111,6 +229,11 @@ protected: for (const auto &element : elements) path += (path.empty() ? "" : ":") + element; + if (path != utils::join(elements, ":")) { + cerr << "utils::join() test failed" << endl; + return TestFail; + } + std::vector<std::string> dirs; for (const auto &dir : utils::split(path, ":")) @@ -121,10 +244,69 @@ protected: return TestFail; } + const auto &split = utils::split(path, ":"); + dirs = std::vector<std::string>{ split.begin(), split.end() }; + + if (dirs != elements) { + cerr << "utils::split() LegacyInputIterator test failed" << endl; + return TestFail; + } + + /* utils::join() with conversion function test. */ + std::vector<Size> sizes = { { 0, 0 }, { 100, 100 } }; + s = utils::join(sizes, "/", [](const Size &size) { + return size.toString(); + }); + + if (s != "0x0/100x100") { + cerr << "utils::join() with conversion test failed" << endl; + return TestFail; + } + /* utils::dirname() tests. */ if (TestPass != testDirname()) return TestFail; + + /* utils::map_keys() test. */ + const std::map<std::string, unsigned int> map{ + { "zero", 0 }, + { "one", 1 }, + { "two", 2 }, + }; + std::vector<std::string> expectedKeys{ + "zero", + "one", + "two", + }; + + std::sort(expectedKeys.begin(), expectedKeys.end()); + + const std::vector<std::string> keys = utils::map_keys(map); + if (keys != expectedKeys) { + cerr << "utils::map_keys() test failed" << endl; + return TestFail; + } + + /* utils::alignUp() and utils::alignDown() tests. */ + if (utils::alignDown(6, 3) != 6 || utils::alignDown(7, 3) != 6) { + cerr << "utils::alignDown test failed" << endl; + return TestFail; + } + + if (utils::alignUp(6, 3) != 6 || utils::alignUp(7, 3) != 9) { + cerr << "utils::alignUp test failed" << endl; + return TestFail; + } + + /* utils::enumerate() test. */ + if (testEnumerate() != TestPass) + return TestFail; + + /* utils::Duration test. */ + if (testDuration() != TestPass) + return TestFail; + return TestPass; } }; diff --git a/test/v4l2_compat/meson.build b/test/v4l2_compat/meson.build new file mode 100644 index 00000000..2691eacf --- /dev/null +++ b/test/v4l2_compat/meson.build @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: CC0-1.0 + +if not is_variable('v4l2_compat') + subdir_done() +endif + +# If ASan is enabled but the ASan runtime shared library is missing, +# v4l2_compat_test.py won't be able to LD_PRELOAD it, resulting in a link order +# runtime check failure as v4l2-ctl and v4l2-compliance are not linked to ASan. +# Skip the test in that case. + +if asan_runtime_missing + warning('Unable to get path to ASan runtime, v4l2_compat test disabled') + subdir_done() +endif + +v4l2_compat_test = files('v4l2_compat_test.py') +v4l2_compat_args = [] + +if asan_enabled + v4l2_compat_args += ['-s', asan_runtime] +endif + +v4l2_compat_args += [v4l2_compat] + +test('v4l2_compat_test', v4l2_compat_test, + args : v4l2_compat_args, + suite : 'v4l2_compat', + timeout : 60) diff --git a/test/v4l2_compat/v4l2_compat_test.py b/test/v4l2_compat/v4l2_compat_test.py new file mode 100755 index 00000000..443babc2 --- /dev/null +++ b/test/v4l2_compat/v4l2_compat_test.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2020, Google Inc. +# +# Author: Paul Elder <paul.elder@ideasonboard.com> +# +# Test the V4L2 compatibility layer + +import argparse +import glob +import os +from packaging import version +import re +import shutil +import signal +import subprocess +import sys + +MIN_V4L_UTILS_VERSION = version.parse("1.21.0") + +TestPass = 0 +TestFail = -1 +TestSkip = 77 + + +supported_pipelines = [ + 'bcm2835-isp', + 'uvcvideo', + 'vimc', +] + + +def grep(exp, arr): + return [s for s in arr if re.search(exp, s)] + + +def run_with_stdout(*args, env={}): + try: + with open(os.devnull, 'w') as devnull: + output = subprocess.check_output(args, env=env, stderr=devnull) + ret = 0 + except subprocess.CalledProcessError as err: + output = err.output + ret = err.returncode + return ret, output.decode('utf-8').split('\n') + + +def extract_result(result): + res = result.split(', ') + ret = {} + ret['total'] = int(res[0].split(': ')[-1]) + ret['succeeded'] = int(res[1].split(': ')[-1]) + ret['failed'] = int(res[2].split(': ')[-1]) + ret['warnings'] = int(res[3].split(': ')[-1]) + ret['device'] = res[0].split()[4].strip(':') + ret['driver'] = res[0].split()[2] + return ret + + +def test_v4l2_compliance(v4l2_compliance, ld_preload, device, base_driver): + ret, output = run_with_stdout(v4l2_compliance, '-s', '-d', device, env={'LD_PRELOAD': ld_preload}) + if ret < 0: + output.append(f'Test for {device} terminated due to signal {signal.Signals(-ret).name}') + return TestFail, output + + result = extract_result(output[-2]) + if result['failed'] == 0: + return TestPass, output + + # vimc will fail s_fmt because it only supports framesizes that are + # multiples of 3 + if base_driver == 'vimc' and result['failed'] == 1: + failures = grep('fail', output) + if re.search('S_FMT cannot handle an invalid format', failures[0]) is None: + return TestFail, output + return TestPass, output + + return TestFail, output + + +def main(argv): + parser = argparse.ArgumentParser() + parser.add_argument('-a', '--all', action='store_true', + help='Test all available cameras') + parser.add_argument('-s', '--sanitizer', type=str, + help='Path to the address sanitizer (ASan) runtime') + parser.add_argument('-v', '--verbose', action='store_true', + help='Make the output verbose') + parser.add_argument('v4l2_compat', type=str, + help='Path to v4l2-compat.so') + args = parser.parse_args(argv[1:]) + + # Compute the LD_PRELOAD value by first loading ASan (if specified) and + # then the V4L2 compat layer. + ld_preload = [] + if args.sanitizer: + ld_preload.append(args.sanitizer) + ld_preload.append(args.v4l2_compat) + ld_preload = ':'.join(ld_preload) + + v4l2_compliance = shutil.which('v4l2-compliance') + if v4l2_compliance is None: + print('v4l2-compliance is not available') + return TestSkip + + ret, out = run_with_stdout(v4l2_compliance, '--version') + if ret != 0 or version.parse(out[0].split()[1].replace(',', '')) < MIN_V4L_UTILS_VERSION: + print('v4l2-compliance version >= 1.21.0 required') + return TestSkip + + v4l2_ctl = shutil.which('v4l2-ctl') + if v4l2_ctl is None: + print('v4l2-ctl is not available') + return TestSkip + + ret, out = run_with_stdout(v4l2_ctl, '--version') + if ret != 0 or version.parse(out[0].split()[-1]) < MIN_V4L_UTILS_VERSION: + print('v4l2-ctl version >= 1.21.0 required') + return TestSkip + + dev_nodes = glob.glob('/dev/video*') + if len(dev_nodes) == 0: + print('no video nodes available to test with') + return TestSkip + + failed = [] + drivers_tested = {} + for device in dev_nodes: + ret, out = run_with_stdout(v4l2_ctl, '-D', '-d', device, env={'LD_PRELOAD': ld_preload}) + if ret < 0: + failed.append(device) + print(f'v4l2-ctl failed on {device} with v4l2-compat') + continue + driver = grep('Driver name', out)[0].split(':')[-1].strip() + if driver != "libcamera": + continue + + ret, out = run_with_stdout(v4l2_ctl, '-D', '-d', device) + if ret < 0: + failed.append(device) + print(f'v4l2-ctl failed on {device} without v4l2-compat') + continue + driver = grep('Driver name', out)[0].split(':')[-1].strip() + if driver not in supported_pipelines: + continue + + # TODO: Add kernel version check when vimc supports scaling + if driver == "vimc": + continue + + if not args.all and driver in drivers_tested: + continue + + print(f'Testing {device} with {driver} driver... ', end='') + ret, msg = test_v4l2_compliance(v4l2_compliance, ld_preload, device, driver) + if ret == TestFail: + failed.append(device) + print('failed') + else: + print('success') + + if ret == TestFail or args.verbose: + print('\n'.join(msg)) + + drivers_tested[driver] = True + + if len(drivers_tested) == 0: + print(f'No compatible drivers found') + return TestSkip + + if len(failed) > 0: + print(f'Failed {len(failed)} tests:') + for device in failed: + print(f'- {device}') + + return TestPass if not failed else TestFail + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/test/v4l2_subdevice/list_formats.cpp b/test/v4l2_subdevice/list_formats.cpp index 067dc5ed..9cbd7b94 100644 --- a/test/v4l2_subdevice/list_formats.cpp +++ b/test/v4l2_subdevice/list_formats.cpp @@ -5,13 +5,15 @@ * libcamera V4L2 Subdevice format handling test */ -#include <iomanip> #include <iostream> #include <vector> #include <libcamera/geometry.h> -#include "v4l2_subdevice.h" +#include <libcamera/base/utils.h> + +#include "libcamera/internal/v4l2_subdevice.h" + #include "v4l2_subdevice_test.h" using namespace std; @@ -35,8 +37,7 @@ void ListFormatsTest::printFormats(unsigned int pad, { cout << "Enumerate formats on pad " << pad << endl; for (const SizeRange &size : sizes) { - cout << " mbus code: 0x" << setfill('0') << setw(4) - << hex << code << endl; + cout << " mbus code: " << utils::hex(code, 4) << endl; cout << " min width: " << dec << size.min.width << endl; cout << " min height: " << dec << size.min.height << endl; cout << " max width: " << dec << size.max.width << endl; @@ -47,29 +48,29 @@ void ListFormatsTest::printFormats(unsigned int pad, int ListFormatsTest::run() { /* List all formats available on existing "Scaler" pads. */ - ImageFormats formats; + V4L2Subdevice::Formats formats; formats = scaler_->formats(0); - if (formats.isEmpty()) { + if (formats.empty()) { cerr << "Failed to list formats on pad 0 of subdevice " << scaler_->entity()->name() << endl; return TestFail; } - for (unsigned int code : formats.formats()) - printFormats(0, code, formats.sizes(code)); + for (unsigned int code : utils::map_keys(formats)) + printFormats(0, code, formats[code]); formats = scaler_->formats(1); - if (formats.isEmpty()) { + if (formats.empty()) { cerr << "Failed to list formats on pad 1 of subdevice " << scaler_->entity()->name() << endl; return TestFail; } - for (unsigned int code : formats.formats()) - printFormats(1, code, formats.sizes(code)); + for (unsigned int code : utils::map_keys(formats)) + printFormats(1, code, formats[code]); /* List format on a non-existing pad, format vector shall be empty. */ formats = scaler_->formats(2); - if (!formats.isEmpty()) { + if (!formats.empty()) { cerr << "Listing formats on non-existing pad 2 of subdevice " << scaler_->entity()->name() << " should return an empty format list" << endl; @@ -79,4 +80,4 @@ int ListFormatsTest::run() return TestPass; } -TEST_REGISTER(ListFormatsTest); +TEST_REGISTER(ListFormatsTest) diff --git a/test/v4l2_subdevice/meson.build b/test/v4l2_subdevice/meson.build index 0521984b..277f29bb 100644 --- a/test/v4l2_subdevice/meson.build +++ b/test/v4l2_subdevice/meson.build @@ -1,12 +1,14 @@ +# SPDX-License-Identifier: CC0-1.0 + v4l2_subdevice_tests = [ - [ 'list_formats', 'list_formats.cpp'], - [ 'test_formats', 'test_formats.cpp'], + {'name': 'list_formats', 'sources': ['list_formats.cpp']}, + {'name': 'test_formats', 'sources': ['test_formats.cpp']}, ] -foreach t : v4l2_subdevice_tests - exe = executable(t[0], [t[1], 'v4l2_subdevice_test.cpp'], - dependencies : libcamera_dep, +foreach test : v4l2_subdevice_tests + exe = executable(test['name'], test['sources'], 'v4l2_subdevice_test.cpp', + dependencies : libcamera_private, link_with : test_libraries, include_directories : test_includes_internal) - test(t[0], exe, suite : 'v4l2_subdevice', is_parallel : false) + test(test['name'], exe, suite : 'v4l2_subdevice', is_parallel : false) endforeach diff --git a/test/v4l2_subdevice/test_formats.cpp b/test/v4l2_subdevice/test_formats.cpp index 5cf5d566..47f979e2 100644 --- a/test/v4l2_subdevice/test_formats.cpp +++ b/test/v4l2_subdevice/test_formats.cpp @@ -8,7 +8,8 @@ #include <iostream> #include <limits.h> -#include "v4l2_subdevice.h" +#include "libcamera/internal/v4l2_subdevice.h" + #include "v4l2_subdevice_test.h" using namespace std; @@ -66,7 +67,7 @@ int FormatHandlingTest::run() return TestFail; } - if (format.size.width == 0 || format.size.height == 0) { + if (format.size.isNull()) { cerr << "Failed to update image format" << endl; return TestFail; } @@ -74,4 +75,4 @@ int FormatHandlingTest::run() return TestPass; } -TEST_REGISTER(FormatHandlingTest); +TEST_REGISTER(FormatHandlingTest) diff --git a/test/v4l2_subdevice/v4l2_subdevice_test.cpp b/test/v4l2_subdevice/v4l2_subdevice_test.cpp index 562a638c..c349c9e3 100644 --- a/test/v4l2_subdevice/v4l2_subdevice_test.cpp +++ b/test/v4l2_subdevice/v4l2_subdevice_test.cpp @@ -2,16 +2,17 @@ /* * Copyright (C) 2019, Google Inc. * - * v4l2_subdevice_test.cpp - VIMC-based V4L2 subdevice test + * VIMC-based V4L2 subdevice test */ #include <iostream> #include <string.h> #include <sys/stat.h> -#include "device_enumerator.h" -#include "media_device.h" -#include "v4l2_subdevice.h" +#include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/media_device.h" +#include "libcamera/internal/v4l2_subdevice.h" + #include "v4l2_subdevice_test.h" using namespace std; diff --git a/test/v4l2_subdevice/v4l2_subdevice_test.h b/test/v4l2_subdevice/v4l2_subdevice_test.h index 3bce6691..89b78302 100644 --- a/test/v4l2_subdevice/v4l2_subdevice_test.h +++ b/test/v4l2_subdevice/v4l2_subdevice_test.h @@ -2,20 +2,18 @@ /* * Copyright (C) 2019, Google Inc. * - * v4l2_subdevice_test.h - VIMC-based V4L2 subdevice test + * VIMC-based V4L2 subdevice test */ -#ifndef __LIBCAMERA_V4L2_SUBDEVICE_TEST_H__ -#define __LIBCAMERA_V4L2_SUBDEVICE_TEST_H__ +#pragma once -#include <libcamera/buffer.h> +#include <libcamera/framebuffer.h> -#include "device_enumerator.h" -#include "media_device.h" -#include "test.h" -#include "v4l2_subdevice.h" +#include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/media_device.h" +#include "libcamera/internal/v4l2_subdevice.h" -using namespace libcamera; +#include "test.h" class V4L2SubdeviceTest : public Test { @@ -29,9 +27,7 @@ protected: int init() override; void cleanup() override; - std::unique_ptr<DeviceEnumerator> enumerator_; - std::shared_ptr<MediaDevice> media_; - V4L2Subdevice *scaler_; + std::unique_ptr<libcamera::DeviceEnumerator> enumerator_; + std::shared_ptr<libcamera::MediaDevice> media_; + libcamera::V4L2Subdevice *scaler_; }; - -#endif /* __LIBCAMERA_V4L2_SUBDEVICE_TEST_H__ */ diff --git a/test/v4l2_videodevice/buffer_cache.cpp b/test/v4l2_videodevice/buffer_cache.cpp index d730e755..5a9aa219 100644 --- a/test/v4l2_videodevice/buffer_cache.cpp +++ b/test/v4l2_videodevice/buffer_cache.cpp @@ -9,6 +9,7 @@ #include <random> #include <vector> +#include <libcamera/formats.h> #include <libcamera/stream.h> #include "buffer_source.h" @@ -125,6 +126,35 @@ public: return TestPass; } + int testIsEmpty(const std::vector<std::unique_ptr<FrameBuffer>> &buffers) + { + V4L2BufferCache cache(buffers.size()); + + if (!cache.isEmpty()) + return TestFail; + + for (auto const &buffer : buffers) { + FrameBuffer &b = *buffer.get(); + cache.get(b); + } + + if (cache.isEmpty()) + return TestFail; + + unsigned int i; + for (i = 0; i < buffers.size() - 1; i++) + cache.put(i); + + if (cache.isEmpty()) + return TestFail; + + cache.put(i); + if (!cache.isEmpty()) + return TestFail; + + return TestPass; + } + int init() override { std::random_device rd; @@ -142,7 +172,7 @@ public: const unsigned int numBuffers = 8; StreamConfiguration cfg; - cfg.pixelFormat = PixelFormat(DRM_FORMAT_YUYV); + cfg.pixelFormat = formats::YUYV; cfg.size = Size(600, 800); cfg.bufferCount = numBuffers; @@ -203,6 +233,13 @@ public: if (testHot(&cacheHalf, buffers, numBuffers / 2) != TestPass) return TestFail; + /* + * Test that the isEmpty function reports the correct result at + * various levels of cache fullness. + */ + if (testIsEmpty(buffers) != TestPass) + return TestFail; + return TestPass; } @@ -212,4 +249,4 @@ private: } /* namespace */ -TEST_REGISTER(BufferCacheTest); +TEST_REGISTER(BufferCacheTest) diff --git a/test/v4l2_videodevice/buffer_sharing.cpp b/test/v4l2_videodevice/buffer_sharing.cpp index 14d3055a..fa856ab6 100644 --- a/test/v4l2_videodevice/buffer_sharing.cpp +++ b/test/v4l2_videodevice/buffer_sharing.cpp @@ -12,13 +12,17 @@ #include <iostream> -#include <libcamera/buffer.h> -#include <libcamera/event_dispatcher.h> -#include <libcamera/timer.h> +#include <libcamera/framebuffer.h> + +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> -#include "thread.h" #include "v4l2_videodevice_test.h" +using namespace libcamera; +using namespace std::chrono_literals; + class BufferSharingTest : public V4L2VideoDeviceTest { public: @@ -142,7 +146,7 @@ protected: return TestFail; } - timeout.start(10000); + timeout.start(10000ms); while (timeout.isRunning()) { dispatcher->processEvents(); if (framesCaptured_ > 30 && framesOutput_ > 30) @@ -199,4 +203,4 @@ private: unsigned int framesOutput_; }; -TEST_REGISTER(BufferSharingTest); +TEST_REGISTER(BufferSharingTest) diff --git a/test/v4l2_videodevice/capture_async.cpp b/test/v4l2_videodevice/capture_async.cpp index b38aabc6..67366461 100644 --- a/test/v4l2_videodevice/capture_async.cpp +++ b/test/v4l2_videodevice/capture_async.cpp @@ -7,13 +7,17 @@ #include <iostream> -#include <libcamera/buffer.h> -#include <libcamera/event_dispatcher.h> -#include <libcamera/timer.h> +#include <libcamera/framebuffer.h> + +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> -#include "thread.h" #include "v4l2_videodevice_test.h" +using namespace libcamera; +using namespace std::chrono_literals; + class CaptureAsyncTest : public V4L2VideoDeviceTest { public: @@ -57,10 +61,12 @@ protected: if (ret) return TestFail; - timeout.start(10000); + const unsigned int nFrames = 30; + + timeout.start(500ms * nFrames); while (timeout.isRunning()) { dispatcher->processEvents(); - if (frames > 30) + if (frames > nFrames) break; } @@ -69,8 +75,9 @@ protected: return TestFail; } - if (frames < 30) { - std::cout << "Failed to capture 30 frames within timeout." << std::endl; + if (frames < nFrames) { + std::cout << "Failed to capture " << nFrames + << " frames within timeout." << std::endl; return TestFail; } @@ -87,4 +94,4 @@ private: unsigned int frames; }; -TEST_REGISTER(CaptureAsyncTest); +TEST_REGISTER(CaptureAsyncTest) diff --git a/test/v4l2_videodevice/controls.cpp b/test/v4l2_videodevice/controls.cpp index da9e0111..b0130295 100644 --- a/test/v4l2_videodevice/controls.cpp +++ b/test/v4l2_videodevice/controls.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2019, Google Inc. * - * controls.cpp - V4L2 device controls handling test + * V4L2 device controls handling test */ #include <algorithm> @@ -10,12 +10,13 @@ #include <iostream> #include <limits.h> -#include "v4l2_videodevice.h" +#include "libcamera/internal/v4l2_videodevice.h" #include "v4l2_videodevice_test.h" /* These come from the vivid driver. */ #define VIVID_CID_CUSTOM_BASE (V4L2_CID_USER_BASE | 0xf000) +#define VIVID_CID_INTEGER64 (VIVID_CID_CUSTOM_BASE + 3) #define VIVID_CID_U8_4D_ARRAY (VIVID_CID_CUSTOM_BASE + 10) /* Helper for VIVID_CID_U8_4D_ARRAY control array size: not from kernel. */ @@ -46,6 +47,7 @@ protected: if (infoMap.find(V4L2_CID_BRIGHTNESS) == infoMap.end() || infoMap.find(V4L2_CID_CONTRAST) == infoMap.end() || infoMap.find(V4L2_CID_SATURATION) == infoMap.end() || + infoMap.find(VIVID_CID_INTEGER64) == infoMap.end() || infoMap.find(VIVID_CID_U8_4D_ARRAY) == infoMap.end()) { cerr << "Missing controls" << endl; return TestFail; @@ -54,21 +56,25 @@ protected: const ControlInfo &brightness = infoMap.find(V4L2_CID_BRIGHTNESS)->second; const ControlInfo &contrast = infoMap.find(V4L2_CID_CONTRAST)->second; const ControlInfo &saturation = infoMap.find(V4L2_CID_SATURATION)->second; + const ControlInfo &int64 = infoMap.find(VIVID_CID_INTEGER64)->second; const ControlInfo &u8 = infoMap.find(VIVID_CID_U8_4D_ARRAY)->second; /* Test getting controls. */ - ControlList ctrls(infoMap); - ctrls.set(V4L2_CID_BRIGHTNESS, -1); - ctrls.set(V4L2_CID_CONTRAST, -1); - ctrls.set(V4L2_CID_SATURATION, -1); - ctrls.set(VIVID_CID_U8_4D_ARRAY, 0); - - int ret = capture_->getControls(&ctrls); - if (ret) { + ControlList ctrls = capture_->getControls({ V4L2_CID_BRIGHTNESS, + V4L2_CID_CONTRAST, + V4L2_CID_SATURATION, + VIVID_CID_INTEGER64, + VIVID_CID_U8_4D_ARRAY }); + if (ctrls.empty()) { cerr << "Failed to get controls" << endl; return TestFail; } + if (ctrls.infoMap() != &infoMap) { + cerr << "Incorrect infoMap for retrieved controls" << endl; + return TestFail; + } + if (ctrls.get(V4L2_CID_BRIGHTNESS).get<int32_t>() == -1 || ctrls.get(V4L2_CID_CONTRAST).get<int32_t>() == -1 || ctrls.get(V4L2_CID_SATURATION).get<int32_t>() == -1) { @@ -76,6 +82,12 @@ protected: return TestFail; } + /* + * The VIVID_CID_INTEGER64 control can take any value, just test + * that its value can be retrieved and has the right type. + */ + ctrls.get(VIVID_CID_INTEGER64).get<int64_t>(); + uint8_t u8Min = u8.min().get<uint8_t>(); uint8_t u8Max = u8.max().get<uint8_t>(); @@ -92,12 +104,13 @@ protected: ctrls.set(V4L2_CID_BRIGHTNESS, brightness.min()); ctrls.set(V4L2_CID_CONTRAST, contrast.max()); ctrls.set(V4L2_CID_SATURATION, saturation.min()); + ctrls.set(VIVID_CID_INTEGER64, int64.min()); std::array<uint8_t, VIVID_CID_U8_ARRAY_SIZE> u8Values; std::fill(u8Values.begin(), u8Values.end(), u8.min().get<uint8_t>()); ctrls.set(VIVID_CID_U8_4D_ARRAY, Span<const uint8_t>(u8Values)); - ret = capture_->setControls(&ctrls); + int ret = capture_->setControls(&ctrls); if (ret) { cerr << "Failed to set controls" << endl; return TestFail; @@ -125,4 +138,4 @@ protected: } }; -TEST_REGISTER(V4L2ControlTest); +TEST_REGISTER(V4L2ControlTest) diff --git a/test/v4l2_videodevice/dequeue_watchdog.cpp b/test/v4l2_videodevice/dequeue_watchdog.cpp new file mode 100644 index 00000000..320d14c8 --- /dev/null +++ b/test/v4l2_videodevice/dequeue_watchdog.cpp @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Ideas on Board Oy. + * + * libcamera V4L2 dequeue watchdog test + */ + +#include <iostream> + +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> + +#include <libcamera/framebuffer.h> + +#include "v4l2_videodevice_test.h" + +using namespace libcamera; +using namespace std::chrono_literals; + +class DequeueWatchdogTest : public V4L2VideoDeviceTest +{ +public: + DequeueWatchdogTest() + : V4L2VideoDeviceTest("vimc", "Raw Capture 0"), frames_(0), barks_(0) {} + +protected: + int run() + { + constexpr unsigned int bufferCount = 8; + + EventDispatcher *dispatcher = Thread::current()->eventDispatcher(); + Timer timeout; + + int ret = capture_->allocateBuffers(bufferCount, &buffers_); + if (ret < 0) { + std::cout << "Failed to allocate buffers" << std::endl; + return TestFail; + } + + capture_->dequeueTimeout.connect(this, &DequeueWatchdogTest::barkCounter); + capture_->setDequeueTimeout(5ms); + + capture_->bufferReady.connect(this, &DequeueWatchdogTest::receiveBuffer); + + for (const std::unique_ptr<FrameBuffer> &buffer : buffers_) { + if (capture_->queueBuffer(buffer.get())) { + std::cout << "Failed to queue buffer" << std::endl; + return TestFail; + } + } + + ret = capture_->streamOn(); + if (ret < 0) { + std::cout << "Failed to start streaming" << std::endl; + return TestFail; + } + + timeout.start(5s); + while (timeout.isRunning()) { + dispatcher->processEvents(); + if (frames_ > 5) + break; + } + + std::cout << "Processed " << frames_ << " frames_ and heard " + << barks_ << " barks_" << std::endl; + + if (!barks_) { + std::cout << "Failed to hear any barks_." << std::endl; + return TestFail; + } + + capture_->streamOff(); + + return TestPass; + } + +private: + void receiveBuffer(FrameBuffer *buffer) + { + if (buffer->metadata().status == FrameMetadata::FrameCancelled) + return; + + std::cout << "Buffer received" << std::endl; + frames_++; + + /* Requeue the buffer for further use. */ + capture_->queueBuffer(buffer); + } + + void barkCounter() + { + std::cout << "Watchdog is barking" << std::endl; + barks_++; + } + + unsigned int frames_; + unsigned int barks_; +}; + +TEST_REGISTER(DequeueWatchdogTest) diff --git a/test/v4l2_videodevice/double_open.cpp b/test/v4l2_videodevice/double_open.cpp index 5768d404..7d11f6fe 100644 --- a/test/v4l2_videodevice/double_open.cpp +++ b/test/v4l2_videodevice/double_open.cpp @@ -38,4 +38,4 @@ protected: } /* namespace */ -TEST_REGISTER(DoubleOpen); +TEST_REGISTER(DoubleOpen) diff --git a/test/v4l2_videodevice/formats.cpp b/test/v4l2_videodevice/formats.cpp index a7421421..6c052622 100644 --- a/test/v4l2_videodevice/formats.cpp +++ b/test/v4l2_videodevice/formats.cpp @@ -8,8 +8,9 @@ #include <iostream> #include <limits.h> -#include "utils.h" -#include "v4l2_videodevice.h" +#include <libcamera/base/utils.h> + +#include "libcamera/internal/v4l2_videodevice.h" #include "v4l2_videodevice_test.h" @@ -55,10 +56,10 @@ protected: { V4L2_PIX_FMT_Y16_BE, "Y16 -BE" } }; - for (const auto &format : formats) { - if (V4L2PixelFormat(format.first).toString() != format.second) { + for (const auto &fmt : formats) { + if (V4L2PixelFormat(fmt.first).toString() != fmt.second) { cerr << "Failed to convert V4L2PixelFormat" - << utils::hex(format.first) << "to string" + << utils::hex(fmt.first) << "to string" << endl; return TestFail; } @@ -74,4 +75,4 @@ protected: } }; -TEST_REGISTER(Format); +TEST_REGISTER(Format) diff --git a/test/v4l2_videodevice/meson.build b/test/v4l2_videodevice/meson.build index 685fcf6d..87ea4f96 100644 --- a/test/v4l2_videodevice/meson.build +++ b/test/v4l2_videodevice/meson.build @@ -1,21 +1,24 @@ +# SPDX-License-Identifier: CC0-1.0 + # Tests are listed in order of complexity. # They are not alphabetically sorted. v4l2_videodevice_tests = [ - [ 'double_open', 'double_open.cpp' ], - [ 'controls', 'controls.cpp' ], - [ 'formats', 'formats.cpp' ], - [ 'request_buffers', 'request_buffers.cpp' ], - [ 'buffer_cache', 'buffer_cache.cpp' ], - [ 'stream_on_off', 'stream_on_off.cpp' ], - [ 'capture_async', 'capture_async.cpp' ], - [ 'buffer_sharing', 'buffer_sharing.cpp' ], - [ 'v4l2_m2mdevice', 'v4l2_m2mdevice.cpp' ], + {'name': 'double_open', 'sources': ['double_open.cpp']}, + {'name': 'controls', 'sources': ['controls.cpp']}, + {'name': 'formats', 'sources': ['formats.cpp']}, + {'name': 'dequeue_watchdog', 'sources': ['dequeue_watchdog.cpp']}, + {'name': 'request_buffers', 'sources': ['request_buffers.cpp']}, + {'name': 'buffer_cache', 'sources': ['buffer_cache.cpp']}, + {'name': 'stream_on_off', 'sources': ['stream_on_off.cpp']}, + {'name': 'capture_async', 'sources': ['capture_async.cpp']}, + {'name': 'buffer_sharing', 'sources': ['buffer_sharing.cpp']}, + {'name': 'v4l2_m2mdevice', 'sources': ['v4l2_m2mdevice.cpp']}, ] -foreach t : v4l2_videodevice_tests - exe = executable(t[0], [t[1], 'v4l2_videodevice_test.cpp'], - dependencies : libcamera_dep, +foreach test : v4l2_videodevice_tests + exe = executable(test['name'], [test['sources'], 'v4l2_videodevice_test.cpp'], + dependencies : libcamera_private, link_with : test_libraries, include_directories : test_includes_internal) - test(t[0], exe, suite : 'v4l2_videodevice', is_parallel : false) + test(test['name'], exe, suite : 'v4l2_videodevice', is_parallel : false) endforeach diff --git a/test/v4l2_videodevice/request_buffers.cpp b/test/v4l2_videodevice/request_buffers.cpp index 2f8dfe1c..fb577147 100644 --- a/test/v4l2_videodevice/request_buffers.cpp +++ b/test/v4l2_videodevice/request_buffers.cpp @@ -26,4 +26,4 @@ protected: } }; -TEST_REGISTER(RequestBuffersTest); +TEST_REGISTER(RequestBuffersTest) diff --git a/test/v4l2_videodevice/stream_on_off.cpp b/test/v4l2_videodevice/stream_on_off.cpp index ce48310a..4ed99e93 100644 --- a/test/v4l2_videodevice/stream_on_off.cpp +++ b/test/v4l2_videodevice/stream_on_off.cpp @@ -33,4 +33,4 @@ protected: } }; -TEST_REGISTER(StreamOnStreamOffTest); +TEST_REGISTER(StreamOnStreamOffTest) diff --git a/test/v4l2_videodevice/v4l2_m2mdevice.cpp b/test/v4l2_videodevice/v4l2_m2mdevice.cpp index d20e5dfc..c45f581a 100644 --- a/test/v4l2_videodevice/v4l2_m2mdevice.cpp +++ b/test/v4l2_videodevice/v4l2_m2mdevice.cpp @@ -7,19 +7,21 @@ #include <iostream> -#include <libcamera/buffer.h> -#include <libcamera/event_dispatcher.h> -#include <libcamera/timer.h> +#include <libcamera/framebuffer.h> -#include "device_enumerator.h" -#include "media_device.h" -#include "thread.h" -#include "v4l2_videodevice.h" +#include <libcamera/base/event_dispatcher.h> +#include <libcamera/base/thread.h> +#include <libcamera/base/timer.h> + +#include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/media_device.h" +#include "libcamera/internal/v4l2_videodevice.h" #include "test.h" -using namespace std; using namespace libcamera; +using namespace std; +using namespace std::chrono_literals; class V4L2M2MDeviceTest : public Test { @@ -93,6 +95,11 @@ protected: V4L2VideoDevice *capture = vim2m_->capture(); V4L2VideoDevice *output = vim2m_->output(); + if (capture->controls().empty() || output->controls().empty()) { + cerr << "VIM2M device has no control" << endl; + return TestFail; + } + V4L2DeviceFormat format = {}; if (capture->getFormat(&format)) { cerr << "Failed to get capture format" << endl; @@ -154,7 +161,7 @@ protected: } Timer timeout; - timeout.start(5000); + timeout.start(5000ms); while (timeout.isRunning()) { dispatcher->processEvents(); if (captureFrames_ > 30) @@ -187,7 +194,7 @@ protected: void cleanup() { delete vim2m_; - }; + } private: std::unique_ptr<DeviceEnumerator> enumerator_; @@ -201,4 +208,4 @@ private: unsigned int captureFrames_; }; -TEST_REGISTER(V4L2M2MDeviceTest); +TEST_REGISTER(V4L2M2MDeviceTest) diff --git a/test/v4l2_videodevice/v4l2_videodevice_test.cpp b/test/v4l2_videodevice/v4l2_videodevice_test.cpp index 93b9e72d..9fbd24cc 100644 --- a/test/v4l2_videodevice/v4l2_videodevice_test.cpp +++ b/test/v4l2_videodevice/v4l2_videodevice_test.cpp @@ -9,10 +9,10 @@ #include <linux/media-bus-format.h> -#include "v4l2_videodevice_test.h" +#include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/media_device.h" -#include "device_enumerator.h" -#include "media_device.h" +#include "v4l2_videodevice_test.h" using namespace std; using namespace libcamera; @@ -60,9 +60,12 @@ int V4L2VideoDeviceTest::init() if (capture_->getFormat(&format)) return TestFail; + format.size.width = 640; + format.size.height = 480; + if (driver_ == "vimc") { - sensor_ = new CameraSensor(media_->getEntityByName("Sensor A")); - if (sensor_->init()) + sensor_ = CameraSensorFactoryBase::create(media_->getEntityByName("Sensor A")); + if (!sensor_) return TestFail; debayer_ = new V4L2Subdevice(media_->getEntityByName("Debayer A")); @@ -72,7 +75,7 @@ int V4L2VideoDeviceTest::init() format.fourcc = V4L2PixelFormat(V4L2_PIX_FMT_SBGGR8); V4L2SubdeviceFormat subformat = {}; - subformat.mbus_code = MEDIA_BUS_FMT_SBGGR8_1X8; + subformat.code = MEDIA_BUS_FMT_SBGGR8_1X8; subformat.size = format.size; if (sensor_->setFormat(&subformat)) @@ -82,8 +85,6 @@ int V4L2VideoDeviceTest::init() return TestFail; } - format.size.width = 640; - format.size.height = 480; if (capture_->setFormat(&format)) return TestFail; @@ -97,6 +98,5 @@ void V4L2VideoDeviceTest::cleanup() capture_->close(); delete debayer_; - delete sensor_; delete capture_; } diff --git a/test/v4l2_videodevice/v4l2_videodevice_test.h b/test/v4l2_videodevice/v4l2_videodevice_test.h index 9acaceb8..7c9003ec 100644 --- a/test/v4l2_videodevice/v4l2_videodevice_test.h +++ b/test/v4l2_videodevice/v4l2_videodevice_test.h @@ -2,24 +2,22 @@ /* * Copyright (C) 2018, Google Inc. * - * vl42device_test.h - libcamera v4l2device test base class + * libcamera v4l2device test base class */ -#ifndef __LIBCAMERA_V4L2_DEVICE_TEST_H_ -#define __LIBCAMERA_V4L2_DEVICE_TEST_H_ -#include <memory> +#pragma once -#include <libcamera/buffer.h> +#include <memory> -#include "test.h" +#include <libcamera/framebuffer.h> -#include "camera_sensor.h" -#include "device_enumerator.h" -#include "media_device.h" -#include "v4l2_subdevice.h" -#include "v4l2_videodevice.h" +#include "libcamera/internal/camera_sensor.h" +#include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/media_device.h" +#include "libcamera/internal/v4l2_subdevice.h" +#include "libcamera/internal/v4l2_videodevice.h" -using namespace libcamera; +#include "test.h" class V4L2VideoDeviceTest : public Test { @@ -36,12 +34,10 @@ protected: std::string driver_; std::string entity_; - std::unique_ptr<DeviceEnumerator> enumerator_; - std::shared_ptr<MediaDevice> media_; - CameraSensor *sensor_; - V4L2Subdevice *debayer_; - V4L2VideoDevice *capture_; - std::vector<std::unique_ptr<FrameBuffer>> buffers_; + std::unique_ptr<libcamera::DeviceEnumerator> enumerator_; + std::shared_ptr<libcamera::MediaDevice> media_; + std::unique_ptr<libcamera::CameraSensor> sensor_; + libcamera::V4L2Subdevice *debayer_; + libcamera::V4L2VideoDevice *capture_; + std::vector<std::unique_ptr<libcamera::FrameBuffer>> buffers_; }; - -#endif /* __LIBCAMERA_V4L2_DEVICE_TEST_H_ */ diff --git a/test/yaml-parser.cpp b/test/yaml-parser.cpp new file mode 100644 index 00000000..1b22c87b --- /dev/null +++ b/test/yaml-parser.cpp @@ -0,0 +1,620 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2022, Google Inc. + * + * YAML parser operations tests + */ + +#include <array> +#include <iostream> +#include <map> +#include <string> +#include <unistd.h> + +#include <libcamera/base/file.h> +#include <libcamera/base/utils.h> + +#include "libcamera/internal/yaml_parser.h" + +#include "test.h" + +using namespace libcamera; +using namespace std; + +static const string testYaml = + "string: libcamera\n" + "double: 3.14159\n" + "int8_t: -100\n" + "uint8_t: 100\n" + "int16_t: -1000\n" + "uint16_t: 1000\n" + "int32_t: -100000\n" + "uint32_t: 100000\n" + "size: [1920, 1080]\n" + "list:\n" + " - James\n" + " - Mary\n" + " - \n" + "dictionary:\n" + " a: 1\n" + " c: 3\n" + " b: 2\n" + " empty:\n" + "level1:\n" + " level2:\n" + " - [1, 2]\n" + " - {one: 1, two: 2}\n"; + +static const string invalidYaml = + "Invalid : - YAML : - Content"; + +class YamlParserTest : public Test +{ +protected: + bool createFile(const string &content, string &filename) + { + filename = "/tmp/libcamera.test.XXXXXX"; + int fd = mkstemp(&filename.front()); + if (fd == -1) + return false; + + int ret = write(fd, content.c_str(), content.size()); + close(fd); + + if (ret != static_cast<int>(content.size())) + return false; + + return true; + } + + int init() + { + if (!createFile(testYaml, testYamlFile_)) + return TestFail; + + if (!createFile(invalidYaml, invalidYamlFile_)) + return TestFail; + + return TestPass; + } + + enum class Type { + String, + Int8, + UInt8, + Int16, + UInt16, + Int32, + UInt32, + Double, + Size, + List, + Dictionary, + }; + + int testObjectType(const YamlObject &obj, const char *name, Type type) + { + bool isList = type == Type::List || type == Type::Size; + bool isScalar = !isList && type != Type::Dictionary; + bool isInteger8 = type == Type::Int8 || type == Type::UInt8; + bool isInteger16 = type == Type::Int16 || type == Type::UInt16; + bool isInteger32 = type == Type::Int32 || type == Type::UInt32; + bool isIntegerUpTo16 = isInteger8 || isInteger16; + bool isIntegerUpTo32 = isIntegerUpTo16 || isInteger32; + bool isSigned = type == Type::Int8 || type == Type::Int16 || + type == Type::Int32; + + if ((isScalar && !obj.isValue()) || (!isScalar && obj.isValue())) { + std::cerr + << "Object " << name << " type mismatch when compared to " + << "value" << std::endl; + return TestFail; + } + + if ((isList && !obj.isList()) || (!isList && obj.isList())) { + std::cerr + << "Object " << name << " type mismatch when compared to " + << "list" << std::endl; + return TestFail; + } + + if ((type == Type::Dictionary && !obj.isDictionary()) || + (type != Type::Dictionary && obj.isDictionary())) { + std::cerr + << "Object " << name << " type mismatch when compared to " + << "dictionary" << std::endl; + return TestFail; + } + + if (!isScalar && obj.get<std::string>()) { + std::cerr + << "Object " << name << " didn't fail to parse as " + << "string" << std::endl; + return TestFail; + } + + if (!isInteger8 && obj.get<int8_t>()) { + std::cerr + << "Object " << name << " didn't fail to parse as " + << "int8_t" << std::endl; + return TestFail; + } + + if ((!isInteger8 || isSigned) && obj.get<uint8_t>()) { + std::cerr + << "Object " << name << " didn't fail to parse as " + << "uint8_t" << std::endl; + return TestFail; + } + + if (!isIntegerUpTo16 && obj.get<int16_t>()) { + std::cerr + << "Object " << name << " didn't fail to parse as " + << "int16_t" << std::endl; + return TestFail; + } + + if ((!isIntegerUpTo16 || isSigned) && obj.get<uint16_t>()) { + std::cerr + << "Object " << name << " didn't fail to parse as " + << "uint16_t" << std::endl; + return TestFail; + } + + if (!isIntegerUpTo32 && obj.get<int32_t>()) { + std::cerr + << "Object " << name << " didn't fail to parse as " + << "int32_t" << std::endl; + return TestFail; + } + + if ((!isIntegerUpTo32 || isSigned) && obj.get<uint32_t>()) { + std::cerr + << "Object " << name << " didn't fail to parse as " + << "uint32_t" << std::endl; + return TestFail; + } + + if (!isIntegerUpTo32 && type != Type::Double && obj.get<double>()) { + std::cerr + << "Object " << name << " didn't fail to parse as " + << "double" << std::endl; + return TestFail; + } + + if (type != Type::Size && obj.get<Size>()) { + std::cerr + << "Object " << name << " didn't fail to parse as " + << "Size" << std::endl; + return TestFail; + } + + return TestPass; + } + + int testIntegerObject(const YamlObject &obj, const char *name, Type type, + int64_t value) + { + uint64_t unsignedValue = static_cast<uint64_t>(value); + std::string strValue = std::to_string(value); + bool isInteger8 = type == Type::Int8 || type == Type::UInt8; + bool isInteger16 = type == Type::Int16 || type == Type::UInt16; + bool isSigned = type == Type::Int8 || type == Type::Int16 || + type == Type::Int32; + + /* All integers can be parsed as strings or double. */ + + if (obj.get<string>().value_or("") != strValue || + obj.get<string>("") != strValue) { + std::cerr + << "Object " << name << " failed to parse as " + << "string" << std::endl; + return TestFail; + } + + if (obj.get<double>().value_or(0.0) != value || + obj.get<double>(0.0) != value) { + std::cerr + << "Object " << name << " failed to parse as " + << "double" << std::endl; + return TestFail; + } + + if (isInteger8) { + if (obj.get<int8_t>().value_or(0) != value || + obj.get<int8_t>(0) != value) { + std::cerr + << "Object " << name << " failed to parse as " + << "int8_t" << std::endl; + return TestFail; + } + } + + if (isInteger8 && !isSigned) { + if (obj.get<uint8_t>().value_or(0) != unsignedValue || + obj.get<uint8_t>(0) != unsignedValue) { + std::cerr + << "Object " << name << " failed to parse as " + << "uint8_t" << std::endl; + return TestFail; + } + } + + if (isInteger8 || isInteger16) { + if (obj.get<int16_t>().value_or(0) != value || + obj.get<int16_t>(0) != value) { + std::cerr + << "Object " << name << " failed to parse as " + << "int16_t" << std::endl; + return TestFail; + } + } + + if ((isInteger8 || isInteger16) && !isSigned) { + if (obj.get<uint16_t>().value_or(0) != unsignedValue || + obj.get<uint16_t>(0) != unsignedValue) { + std::cerr + << "Object " << name << " failed to parse as " + << "uint16_t" << std::endl; + return TestFail; + } + } + + if (obj.get<int32_t>().value_or(0) != value || + obj.get<int32_t>(0) != value) { + std::cerr + << "Object " << name << " failed to parse as " + << "int32_t" << std::endl; + return TestFail; + } + + if (!isSigned) { + if (obj.get<uint32_t>().value_or(0) != unsignedValue || + obj.get<uint32_t>(0) != unsignedValue) { + std::cerr + << "Object " << name << " failed to parse as " + << "uint32_t" << std::endl; + return TestFail; + } + } + + return TestPass; + } + + int run() + { + /* Test invalid YAML file */ + File file{ invalidYamlFile_ }; + if (!file.open(File::OpenModeFlag::ReadOnly)) { + cerr << "Fail to open invalid YAML file" << std::endl; + return TestFail; + } + + std::unique_ptr<YamlObject> root = YamlParser::parse(file); + if (root) { + cerr << "Invalid YAML file parse successfully" << std::endl; + return TestFail; + } + + /* Test YAML file */ + file.close(); + file.setFileName(testYamlFile_); + if (!file.open(File::OpenModeFlag::ReadOnly)) { + cerr << "Fail to open test YAML file" << std::endl; + return TestFail; + } + + root = YamlParser::parse(file); + + if (!root) { + cerr << "Fail to parse test YAML file: " << std::endl; + return TestFail; + } + + if (!root->isDictionary()) { + cerr << "YAML root is not dictionary" << std::endl; + return TestFail; + } + + std::vector<const char *> rootElemNames = { + "string", "double", "int8_t", "uint8_t", "int16_t", + "uint16_t", "int32_t", "uint32_t", "size", "list", + "dictionary", "level1", + }; + + for (const char *name : rootElemNames) { + if (!root->contains(name)) { + cerr << "Missing " << name << " object in YAML root" + << std::endl; + return TestFail; + } + } + + /* Test string object */ + auto &strObj = (*root)["string"]; + + if (testObjectType(strObj, "string", Type::String) != TestPass) + return TestFail; + + if (strObj.get<string>().value_or("") != "libcamera" || + strObj.get<string>("") != "libcamera") { + cerr << "String object parse as wrong content" << std::endl; + return TestFail; + } + + /* Test int8_t object */ + auto &int8Obj = (*root)["int8_t"]; + + if (testObjectType(int8Obj, "int8_t", Type::Int8) != TestPass) + return TestFail; + + if (testIntegerObject(int8Obj, "int8_t", Type::Int8, -100) != TestPass) + return TestFail; + + /* Test uint8_t object */ + auto &uint8Obj = (*root)["uint8_t"]; + + if (testObjectType(uint8Obj, "uint8_t", Type::UInt8) != TestPass) + return TestFail; + + if (testIntegerObject(uint8Obj, "uint8_t", Type::UInt8, 100) != TestPass) + return TestFail; + + /* Test int16_t object */ + auto &int16Obj = (*root)["int16_t"]; + + if (testObjectType(int16Obj, "int16_t", Type::Int16) != TestPass) + return TestFail; + + if (testIntegerObject(int16Obj, "int16_t", Type::Int16, -1000) != TestPass) + return TestFail; + + /* Test uint16_t object */ + auto &uint16Obj = (*root)["uint16_t"]; + + if (testObjectType(uint16Obj, "uint16_t", Type::UInt16) != TestPass) + return TestFail; + + if (testIntegerObject(uint16Obj, "uint16_t", Type::UInt16, 1000) != TestPass) + return TestFail; + + /* Test int32_t object */ + auto &int32Obj = (*root)["int32_t"]; + + if (testObjectType(int32Obj, "int32_t", Type::Int32) != TestPass) + return TestFail; + + if (testIntegerObject(int32Obj, "int32_t", Type::Int32, -100000) != TestPass) + return TestFail; + + /* Test uint32_t object */ + auto &uint32Obj = (*root)["uint32_t"]; + + if (testObjectType(uint32Obj, "uint32_t", Type::UInt32) != TestPass) + return TestFail; + + if (testIntegerObject(uint32Obj, "uint32_t", Type::UInt32, 100000) != TestPass) + return TestFail; + + /* Test double value */ + auto &doubleObj = (*root)["double"]; + + if (testObjectType(doubleObj, "double", Type::Double) != TestPass) + return TestFail; + + if (doubleObj.get<string>().value_or("") != "3.14159" || + doubleObj.get<string>("") != "3.14159") { + cerr << "Double object fail to parse as string" << std::endl; + return TestFail; + } + + if (doubleObj.get<double>().value_or(0.0) != 3.14159 || + doubleObj.get<double>(0.0) != 3.14159) { + cerr << "Double object parse as wrong value" << std::endl; + return TestFail; + } + + /* Test Size value */ + auto &sizeObj = (*root)["size"]; + + if (testObjectType(sizeObj, "size", Type::Size) != TestPass) + return TestFail; + + if (sizeObj.get<Size>().value_or(Size(0, 0)) != Size(1920, 1080) || + sizeObj.get<Size>(Size(0, 0)) != Size(1920, 1080)) { + cerr << "Size object parse as wrong value" << std::endl; + return TestFail; + } + + /* Test list object */ + auto &listObj = (*root)["list"]; + + if (testObjectType(listObj, "list", Type::List) != TestPass) + return TestFail; + + static constexpr std::array<const char *, 3> listValues{ + "James", + "Mary", + "", + }; + + if (listObj.size() != listValues.size()) { + cerr << "List object parse with wrong size" << std::endl; + return TestFail; + } + + unsigned int i = 0; + for (auto &elem : listObj.asList()) { + if (i >= listValues.size()) { + std::cerr << "Too many elements in list during iteration" + << std::endl; + return TestFail; + } + + std::string value = listValues[i]; + + if (&elem != &listObj[i]) { + std::cerr << "List element " << i << " has wrong address" + << std::endl; + return TestFail; + } + + if (elem.get<std::string>("") != value) { + std::cerr << "List element " << i << " has wrong value" + << std::endl; + return TestFail; + } + + i++; + } + + /* Ensure that empty objects get parsed as empty strings. */ + if (!listObj[2].isValue()) { + cerr << "Empty object is not a value" << std::endl; + return TestFail; + } + + /* Test dictionary object */ + auto &dictObj = (*root)["dictionary"]; + + if (testObjectType(dictObj, "dictionary", Type::Dictionary) != TestPass) + return TestFail; + + static constexpr std::array<std::pair<const char *, int>, 4> dictValues{ { + { "a", 1 }, + { "c", 3 }, + { "b", 2 }, + { "empty", -100 }, + } }; + + size_t dictSize = dictValues.size(); + + if (dictObj.size() != dictSize) { + cerr << "Dictionary object has wrong size" << std::endl; + return TestFail; + } + + i = 0; + for (const auto &[key, elem] : dictObj.asDict()) { + if (i >= dictSize) { + std::cerr << "Too many elements in dictionary during iteration" + << std::endl; + return TestFail; + } + + const auto &item = dictValues[i]; + if (item.first != key) { + std::cerr << "Dictionary key " << i << " has wrong value" + << std::endl; + return TestFail; + } + + if (&elem != &dictObj[key]) { + std::cerr << "Dictionary element " << i << " has wrong address" + << std::endl; + return TestFail; + } + + if (elem.get<int32_t>(-100) != item.second) { + std::cerr << "Dictionary element " << i << " has wrong value" + << std::endl; + return TestFail; + } + + i++; + } + + /* Ensure that empty objects get parsed as empty strings. */ + if (!dictObj["empty"].isValue()) { + cerr << "Empty object is not of type value" << std::endl; + return TestFail; + } + + /* Ensure that keys without values are added to a dict. */ + if (!dictObj.contains("empty")) { + cerr << "Empty element is missing in dict" << std::endl; + return TestFail; + } + + /* Test access to nonexistent member. */ + if (dictObj["nonexistent"].get<std::string>("default") != "default") { + cerr << "Accessing nonexistent dict entry fails to return default" << std::endl; + return TestFail; + } + + /* Test nonexistent object has value type empty. */ + if (!dictObj["nonexistent"].isEmpty()) { + cerr << "Accessing nonexistent object returns non-empty object" << std::endl; + return TestFail; + } + + /* Test explicit cast to bool on an empty object returns true. */ + if (!!dictObj["empty"] != true) { + cerr << "Casting empty entry to bool returns false" << std::endl; + return TestFail; + } + + /* Test explicit cast to bool on nonexistent object returns false. */ + if (!!dictObj["nonexistent"] != false) { + cerr << "Casting nonexistent dict entry to bool returns true" << std::endl; + return TestFail; + } + + /* Make sure utils::map_keys() works on the adapter. */ + (void)utils::map_keys(dictObj.asDict()); + + /* Test leveled objects */ + auto &level1Obj = (*root)["level1"]; + + if (!level1Obj.isDictionary()) { + cerr << "level1 object fail to parse as Dictionary" << std::endl; + return TestFail; + } + + auto &level2Obj = level1Obj["level2"]; + + if (!level2Obj.isList() || level2Obj.size() != 2) { + cerr << "level2 object should be 2 element list" << std::endl; + return TestFail; + } + + auto &firstElement = level2Obj[0]; + if (!firstElement.isList() || + firstElement.size() != 2 || + firstElement[0].get<int32_t>(0) != 1 || + firstElement[1].get<int32_t>(0) != 2) { + cerr << "The first element of level2 object fail to parse as integer list" << std::endl; + return TestFail; + } + + const auto &values = firstElement.getList<uint16_t>(); + if (!values || values->size() != 2 || (*values)[0] != 1 || (*values)[1] != 2) { + cerr << "getList() failed to return correct vector" << std::endl; + return TestFail; + } + + auto &secondElement = level2Obj[1]; + if (!secondElement.isDictionary() || + !secondElement.contains("one") || + !secondElement.contains("two") || + secondElement["one"].get<int32_t>(0) != 1 || + secondElement["two"].get<int32_t>(0) != 2) { + cerr << "The second element of level2 object fail to parse as dictionary" << std::endl; + return TestFail; + } + + return TestPass; + } + + void cleanup() + { + unlink(testYamlFile_.c_str()); + unlink(invalidYamlFile_.c_str()); + } + +private: + std::string testYamlFile_; + std::string invalidYamlFile_; +}; + +TEST_REGISTER(YamlParserTest) |