summaryrefslogtreecommitdiff
path: root/src/apps/lc-compliance
diff options
context:
space:
mode:
Diffstat (limited to 'src/apps/lc-compliance')
-rw-r--r--src/apps/lc-compliance/environment.cpp22
-rw-r--r--src/apps/lc-compliance/environment.h27
-rw-r--r--src/apps/lc-compliance/helpers/capture.cpp196
-rw-r--r--src/apps/lc-compliance/helpers/capture.h66
-rw-r--r--src/apps/lc-compliance/main.cpp194
-rw-r--r--src/apps/lc-compliance/meson.build37
-rw-r--r--src/apps/lc-compliance/tests/capture_test.cpp136
7 files changed, 678 insertions, 0 deletions
diff --git a/src/apps/lc-compliance/environment.cpp b/src/apps/lc-compliance/environment.cpp
new file mode 100644
index 00000000..987264f1
--- /dev/null
+++ b/src/apps/lc-compliance/environment.cpp
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Collabora Ltd.
+ *
+ * Common environment for tests
+ */
+
+#include "environment.h"
+
+using namespace libcamera;
+
+Environment *Environment::get()
+{
+ static Environment instance;
+ return &instance;
+}
+
+void Environment::setup(CameraManager *cm, std::string cameraId)
+{
+ cm_ = cm;
+ cameraId_ = cameraId;
+}
diff --git a/src/apps/lc-compliance/environment.h b/src/apps/lc-compliance/environment.h
new file mode 100644
index 00000000..543e5372
--- /dev/null
+++ b/src/apps/lc-compliance/environment.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Collabora Ltd.
+ *
+ * Common environment for tests
+ */
+
+#pragma once
+
+#include <libcamera/libcamera.h>
+
+class Environment
+{
+public:
+ static Environment *get();
+
+ void setup(libcamera::CameraManager *cm, std::string cameraId);
+
+ const std::string &cameraId() const { return cameraId_; }
+ libcamera::CameraManager *cm() const { return cm_; }
+
+private:
+ Environment() = default;
+
+ std::string cameraId_;
+ libcamera::CameraManager *cm_;
+};
diff --git a/src/apps/lc-compliance/helpers/capture.cpp b/src/apps/lc-compliance/helpers/capture.cpp
new file mode 100644
index 00000000..90c1530b
--- /dev/null
+++ b/src/apps/lc-compliance/helpers/capture.cpp
@@ -0,0 +1,196 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020-2021, Google Inc.
+ *
+ * Simple capture helper
+ */
+
+#include "capture.h"
+
+#include <gtest/gtest.h>
+
+using namespace libcamera;
+
+Capture::Capture(std::shared_ptr<Camera> camera)
+ : loop_(nullptr), camera_(camera),
+ allocator_(std::make_unique<FrameBufferAllocator>(camera))
+{
+}
+
+Capture::~Capture()
+{
+ stop();
+}
+
+void Capture::configure(StreamRole role)
+{
+ config_ = camera_->generateConfiguration({ role });
+
+ if (!config_) {
+ std::cout << "Role not supported by camera" << std::endl;
+ GTEST_SKIP();
+ }
+
+ if (config_->validate() != CameraConfiguration::Valid) {
+ config_.reset();
+ FAIL() << "Configuration not valid";
+ }
+
+ if (camera_->configure(config_.get())) {
+ config_.reset();
+ FAIL() << "Failed to configure camera";
+ }
+}
+
+void Capture::start()
+{
+ Stream *stream = config_->at(0).stream();
+ int count = allocator_->allocate(stream);
+
+ ASSERT_GE(count, 0) << "Failed to allocate buffers";
+ EXPECT_EQ(count, config_->at(0).bufferCount) << "Allocated less buffers than expected";
+
+ camera_->requestCompleted.connect(this, &Capture::requestComplete);
+
+ ASSERT_EQ(camera_->start(), 0) << "Failed to start camera";
+}
+
+void Capture::stop()
+{
+ if (!config_ || !allocator_->allocated())
+ return;
+
+ camera_->stop();
+
+ camera_->requestCompleted.disconnect(this);
+
+ Stream *stream = config_->at(0).stream();
+ requests_.clear();
+ allocator_->free(stream);
+}
+
+/* CaptureBalanced */
+
+CaptureBalanced::CaptureBalanced(std::shared_ptr<Camera> camera)
+ : Capture(camera)
+{
+}
+
+void CaptureBalanced::capture(unsigned int numRequests)
+{
+ start();
+
+ Stream *stream = config_->at(0).stream();
+ const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream);
+
+ /* No point in testing less requests then the camera depth. */
+ if (buffers.size() > numRequests) {
+ std::cout << "Camera needs " + std::to_string(buffers.size())
+ + " requests, can't test only "
+ + std::to_string(numRequests) << std::endl;
+ GTEST_SKIP();
+ }
+
+ queueCount_ = 0;
+ captureCount_ = 0;
+ captureLimit_ = numRequests;
+
+ /* Queue the recommended number of requests. */
+ for (const std::unique_ptr<FrameBuffer> &buffer : buffers) {
+ std::unique_ptr<Request> request = camera_->createRequest();
+ ASSERT_TRUE(request) << "Can't create request";
+
+ ASSERT_EQ(request->addBuffer(stream, buffer.get()), 0) << "Can't set buffer for request";
+
+ ASSERT_EQ(queueRequest(request.get()), 0) << "Failed to queue request";
+
+ requests_.push_back(std::move(request));
+ }
+
+ /* Run capture session. */
+ loop_ = new EventLoop();
+ loop_->exec();
+ stop();
+ delete loop_;
+
+ ASSERT_EQ(captureCount_, captureLimit_);
+}
+
+int CaptureBalanced::queueRequest(Request *request)
+{
+ queueCount_++;
+ if (queueCount_ > captureLimit_)
+ return 0;
+
+ return camera_->queueRequest(request);
+}
+
+void CaptureBalanced::requestComplete(Request *request)
+{
+ EXPECT_EQ(request->status(), Request::Status::RequestComplete)
+ << "Request didn't complete successfully";
+
+ captureCount_++;
+ if (captureCount_ >= captureLimit_) {
+ loop_->exit(0);
+ return;
+ }
+
+ request->reuse(Request::ReuseBuffers);
+ if (queueRequest(request))
+ loop_->exit(-EINVAL);
+}
+
+/* CaptureUnbalanced */
+
+CaptureUnbalanced::CaptureUnbalanced(std::shared_ptr<Camera> camera)
+ : Capture(camera)
+{
+}
+
+void CaptureUnbalanced::capture(unsigned int numRequests)
+{
+ start();
+
+ Stream *stream = config_->at(0).stream();
+ const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream);
+
+ captureCount_ = 0;
+ captureLimit_ = numRequests;
+
+ /* Queue the recommended number of requests. */
+ for (const std::unique_ptr<FrameBuffer> &buffer : buffers) {
+ std::unique_ptr<Request> request = camera_->createRequest();
+ ASSERT_TRUE(request) << "Can't create request";
+
+ ASSERT_EQ(request->addBuffer(stream, buffer.get()), 0) << "Can't set buffer for request";
+
+ ASSERT_EQ(camera_->queueRequest(request.get()), 0) << "Failed to queue request";
+
+ requests_.push_back(std::move(request));
+ }
+
+ /* Run capture session. */
+ loop_ = new EventLoop();
+ int status = loop_->exec();
+ stop();
+ delete loop_;
+
+ ASSERT_EQ(status, 0);
+}
+
+void CaptureUnbalanced::requestComplete(Request *request)
+{
+ captureCount_++;
+ if (captureCount_ >= captureLimit_) {
+ loop_->exit(0);
+ return;
+ }
+
+ EXPECT_EQ(request->status(), Request::Status::RequestComplete)
+ << "Request didn't complete successfully";
+
+ request->reuse(Request::ReuseBuffers);
+ if (camera_->queueRequest(request))
+ loop_->exit(-EINVAL);
+}
diff --git a/src/apps/lc-compliance/helpers/capture.h b/src/apps/lc-compliance/helpers/capture.h
new file mode 100644
index 00000000..19b6927c
--- /dev/null
+++ b/src/apps/lc-compliance/helpers/capture.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020-2021, Google Inc.
+ *
+ * Simple capture helper
+ */
+
+#pragma once
+
+#include <memory>
+
+#include <libcamera/libcamera.h>
+
+#include "../common/event_loop.h"
+
+class Capture
+{
+public:
+ void configure(libcamera::StreamRole role);
+
+protected:
+ Capture(std::shared_ptr<libcamera::Camera> camera);
+ virtual ~Capture();
+
+ void start();
+ void stop();
+
+ virtual void requestComplete(libcamera::Request *request) = 0;
+
+ EventLoop *loop_;
+
+ std::shared_ptr<libcamera::Camera> camera_;
+ std::unique_ptr<libcamera::FrameBufferAllocator> allocator_;
+ std::unique_ptr<libcamera::CameraConfiguration> config_;
+ std::vector<std::unique_ptr<libcamera::Request>> requests_;
+};
+
+class CaptureBalanced : public Capture
+{
+public:
+ CaptureBalanced(std::shared_ptr<libcamera::Camera> camera);
+
+ void capture(unsigned int numRequests);
+
+private:
+ int queueRequest(libcamera::Request *request);
+ void requestComplete(libcamera::Request *request) override;
+
+ unsigned int queueCount_;
+ unsigned int captureCount_;
+ unsigned int captureLimit_;
+};
+
+class CaptureUnbalanced : public Capture
+{
+public:
+ CaptureUnbalanced(std::shared_ptr<libcamera::Camera> camera);
+
+ void capture(unsigned int numRequests);
+
+private:
+ void requestComplete(libcamera::Request *request) override;
+
+ unsigned int captureCount_;
+ unsigned int captureLimit_;
+};
diff --git a/src/apps/lc-compliance/main.cpp b/src/apps/lc-compliance/main.cpp
new file mode 100644
index 00000000..3f1d2a61
--- /dev/null
+++ b/src/apps/lc-compliance/main.cpp
@@ -0,0 +1,194 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ * Copyright (C) 2021, Collabora Ltd.
+ *
+ * lc-compliance - The libcamera compliance tool
+ */
+
+#include <iomanip>
+#include <iostream>
+#include <string.h>
+
+#include <gtest/gtest.h>
+
+#include <libcamera/libcamera.h>
+
+#include "../common/options.h"
+
+#include "environment.h"
+
+using namespace libcamera;
+
+enum {
+ OptCamera = 'c',
+ OptList = 'l',
+ OptFilter = 'f',
+ OptHelp = 'h',
+};
+
+/*
+ * Make asserts act like exceptions, otherwise they only fail (or skip) the
+ * current function. From gtest documentation:
+ * https://google.github.io/googletest/advanced.html#asserting-on-subroutines-with-an-exception
+ */
+class ThrowListener : public testing::EmptyTestEventListener
+{
+ void OnTestPartResult(const testing::TestPartResult &result) override
+ {
+ if (result.type() == testing::TestPartResult::kFatalFailure ||
+ result.type() == testing::TestPartResult::kSkip)
+ throw testing::AssertionException(result);
+ }
+};
+
+static void listCameras(CameraManager *cm)
+{
+ for (const std::shared_ptr<Camera> &cam : cm->cameras())
+ std::cout << "- " << cam.get()->id() << std::endl;
+}
+
+static int initCamera(CameraManager *cm, OptionsParser::Options options)
+{
+ std::shared_ptr<Camera> camera;
+
+ int ret = cm->start();
+ if (ret) {
+ std::cout << "Failed to start camera manager: "
+ << strerror(-ret) << std::endl;
+ return ret;
+ }
+
+ if (!options.isSet(OptCamera)) {
+ std::cout << "No camera specified, available cameras:" << std::endl;
+ listCameras(cm);
+ return -ENODEV;
+ }
+
+ const std::string &cameraId = options[OptCamera];
+ camera = cm->get(cameraId);
+ if (!camera) {
+ std::cout << "Camera " << cameraId << " not found, available cameras:" << std::endl;
+ listCameras(cm);
+ return -ENODEV;
+ }
+
+ Environment::get()->setup(cm, cameraId);
+
+ std::cout << "Using camera " << cameraId << std::endl;
+
+ return 0;
+}
+
+static int initGtestParameters(char *arg0, OptionsParser::Options options)
+{
+ const std::map<std::string, std::string> gtestFlags = { { "list", "--gtest_list_tests" },
+ { "filter", "--gtest_filter" } };
+
+ int argc = 0;
+ std::string filterParam;
+
+ /*
+ * +2 to have space for both the 0th argument that is needed but not
+ * used and the null at the end.
+ */
+ char **argv = new char *[(gtestFlags.size() + 2)];
+ if (!argv)
+ return -ENOMEM;
+
+ argv[0] = arg0;
+ argc++;
+
+ if (options.isSet(OptList)) {
+ argv[argc] = const_cast<char *>(gtestFlags.at("list").c_str());
+ argc++;
+ }
+
+ if (options.isSet(OptFilter)) {
+ /*
+ * The filter flag needs to be passed as a single parameter, in
+ * the format --gtest_filter=filterStr
+ */
+ filterParam = gtestFlags.at("filter") + "=" +
+ static_cast<const std::string &>(options[OptFilter]);
+
+ argv[argc] = const_cast<char *>(filterParam.c_str());
+ argc++;
+ }
+
+ argv[argc] = nullptr;
+
+ ::testing::InitGoogleTest(&argc, argv);
+
+ delete[] argv;
+
+ return 0;
+}
+
+static int initGtest(char *arg0, OptionsParser::Options options)
+{
+ int ret = initGtestParameters(arg0, options);
+ if (ret)
+ return ret;
+
+ testing::UnitTest::GetInstance()->listeners().Append(new ThrowListener);
+
+ return 0;
+}
+
+static int parseOptions(int argc, char **argv, OptionsParser::Options *options)
+{
+ OptionsParser parser;
+ parser.addOption(OptCamera, OptionString,
+ "Specify which camera to operate on, by id", "camera",
+ ArgumentRequired, "camera");
+ parser.addOption(OptList, OptionNone, "List all tests and exit", "list");
+ parser.addOption(OptFilter, OptionString,
+ "Specify which tests to run", "filter",
+ ArgumentRequired, "filter");
+ parser.addOption(OptHelp, OptionNone, "Display this help message",
+ "help");
+
+ *options = parser.parse(argc, argv);
+ if (!options->valid())
+ return -EINVAL;
+
+ if (options->isSet(OptHelp)) {
+ parser.usage();
+ std::cerr << "Further options from Googletest can be passed as environment variables"
+ << std::endl;
+ return -EINTR;
+ }
+
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ OptionsParser::Options options;
+ int ret = parseOptions(argc, argv, &options);
+ if (ret == -EINTR)
+ return EXIT_SUCCESS;
+ if (ret < 0)
+ return EXIT_FAILURE;
+
+ std::unique_ptr<CameraManager> cm = std::make_unique<CameraManager>();
+
+ /* No need to initialize the camera if we'll just list tests */
+ if (!options.isSet(OptList)) {
+ ret = initCamera(cm.get(), options);
+ if (ret)
+ return ret;
+ }
+
+ ret = initGtest(argv[0], options);
+ if (ret)
+ return ret;
+
+ ret = RUN_ALL_TESTS();
+
+ if (!options.isSet(OptList))
+ cm->stop();
+
+ return ret;
+}
diff --git a/src/apps/lc-compliance/meson.build b/src/apps/lc-compliance/meson.build
new file mode 100644
index 00000000..b1f605f3
--- /dev/null
+++ b/src/apps/lc-compliance/meson.build
@@ -0,0 +1,37 @@
+# SPDX-License-Identifier: CC0-1.0
+
+libgtest = dependency('gtest', version : '>=1.10.0',
+ required : get_option('lc-compliance'),
+ fallback : ['gtest', 'gtest_dep'])
+
+if opt_lc_compliance.disabled() or not libevent.found() or not libgtest.found()
+ lc_compliance_enabled = false
+ subdir_done()
+endif
+
+lc_compliance_enabled = true
+
+lc_compliance_sources = files([
+ 'environment.cpp',
+ 'helpers/capture.cpp',
+ 'main.cpp',
+ 'tests/capture_test.cpp',
+])
+
+lc_compliance_includes = ([
+ include_directories('.'),
+ include_directories('helpers/')
+])
+
+lc_compliance = executable('lc-compliance', lc_compliance_sources,
+ cpp_args : [ '-fexceptions' ],
+ link_with : apps_lib,
+ dependencies : [
+ libatomic,
+ libcamera_public,
+ libevent,
+ libgtest,
+ ],
+ include_directories : lc_compliance_includes,
+ install : true,
+ install_tag : 'bin-devel')
diff --git a/src/apps/lc-compliance/tests/capture_test.cpp b/src/apps/lc-compliance/tests/capture_test.cpp
new file mode 100644
index 00000000..ad3a1da2
--- /dev/null
+++ b/src/apps/lc-compliance/tests/capture_test.cpp
@@ -0,0 +1,136 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ * Copyright (C) 2021, Collabora Ltd.
+ *
+ * Test camera capture
+ */
+
+#include "capture.h"
+
+#include <iostream>
+
+#include <gtest/gtest.h>
+
+#include "environment.h"
+
+using namespace libcamera;
+
+const std::vector<int> NUMREQUESTS = { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
+const std::vector<StreamRole> ROLES = {
+ StreamRole::Raw,
+ StreamRole::StillCapture,
+ StreamRole::VideoRecording,
+ StreamRole::Viewfinder
+};
+
+class SingleStream : public testing::TestWithParam<std::tuple<StreamRole, int>>
+{
+public:
+ static std::string nameParameters(const testing::TestParamInfo<SingleStream::ParamType> &info);
+
+protected:
+ void SetUp() override;
+ void TearDown() override;
+
+ std::shared_ptr<Camera> camera_;
+};
+
+/*
+ * We use gtest's SetUp() and TearDown() instead of constructor and destructor
+ * in order to be able to assert on them.
+ */
+void SingleStream::SetUp()
+{
+ Environment *env = Environment::get();
+
+ camera_ = env->cm()->get(env->cameraId());
+
+ ASSERT_EQ(camera_->acquire(), 0);
+}
+
+void SingleStream::TearDown()
+{
+ if (!camera_)
+ return;
+
+ camera_->release();
+ camera_.reset();
+}
+
+std::string SingleStream::nameParameters(const testing::TestParamInfo<SingleStream::ParamType> &info)
+{
+ std::map<StreamRole, std::string> rolesMap = {
+ { StreamRole::Raw, "Raw" },
+ { StreamRole::StillCapture, "StillCapture" },
+ { StreamRole::VideoRecording, "VideoRecording" },
+ { StreamRole::Viewfinder, "Viewfinder" }
+ };
+
+ std::string roleName = rolesMap[std::get<0>(info.param)];
+ std::string numRequestsName = std::to_string(std::get<1>(info.param));
+
+ return roleName + "_" + numRequestsName;
+}
+
+/*
+ * Test single capture cycles
+ *
+ * Makes sure the camera completes the exact number of requests queued. Example
+ * failure is a camera that completes less requests than the number of requests
+ * queued.
+ */
+TEST_P(SingleStream, Capture)
+{
+ auto [role, numRequests] = GetParam();
+
+ CaptureBalanced capture(camera_);
+
+ capture.configure(role);
+
+ capture.capture(numRequests);
+}
+
+/*
+ * Test multiple start/stop cycles
+ *
+ * Makes sure the camera supports multiple start/stop cycles. Example failure is
+ * a camera that does not clean up correctly in its error path but is only
+ * tested by single-capture applications.
+ */
+TEST_P(SingleStream, CaptureStartStop)
+{
+ auto [role, numRequests] = GetParam();
+ unsigned int numRepeats = 3;
+
+ CaptureBalanced capture(camera_);
+
+ capture.configure(role);
+
+ for (unsigned int starts = 0; starts < numRepeats; starts++)
+ capture.capture(numRequests);
+}
+
+/*
+ * Test unbalanced stop
+ *
+ * Makes sure the camera supports a stop with requests queued. Example failure
+ * is a camera that does not handle cancelation of buffers coming back from the
+ * video device while stopping.
+ */
+TEST_P(SingleStream, UnbalancedStop)
+{
+ auto [role, numRequests] = GetParam();
+
+ CaptureUnbalanced capture(camera_);
+
+ capture.configure(role);
+
+ capture.capture(numRequests);
+}
+
+INSTANTIATE_TEST_SUITE_P(CaptureTests,
+ SingleStream,
+ testing::Combine(testing::ValuesIn(ROLES),
+ testing::ValuesIn(NUMREQUESTS)),
+ SingleStream::nameParameters);