[libcamera-devel] [RFC PATCH v2 3/3] lc-compliance: Refactor using Googletest

Niklas Söderlund niklas.soderlund at ragnatech.se
Wed May 12 10:56:36 CEST 2021


On 2021-05-11 16:06:04 -0300, Nícolas F. R. A. Prado wrote:
> Hello again,

Hello :-)

> 
> Em 2021-05-11 14:17, Nícolas F. R. A. Prado escreveu:
> 
> > Hi Niklas,
> >
> > Em 2021-05-10 18:04, Niklas Söderlund escreveu:
> >
> > > Hi Nícolas,
> > >
> > > Thanks for your work on this. I really think gtest is a step in the
> > > right direction!
> > >
> > > On 2021-05-07 12:52:38 -0300, Nícolas F. R. A. Prado wrote:
> > > > Hi Niklas,
> > > > 
> > > > Em 2021-05-07 06:11, Niklas Söderlund escreveu:
> > > > 
> > > > > Hi Nícolas,
> > > > >
> > > > > On 2021-05-06 17:20:29 -0300, Nícolas F. R. A. Prado wrote:
> > > > > > Hi Niklas,
> > > > > > 
> > > > > > thanks for the feedback.
> > > > > > 
> > > > > > Em 2021-05-06 05:48, Niklas Söderlund escreveu:
> > > > > > 
> > > > > > > Hi Nícolas,
> > > > > > >
> > > > > > > Thanks for your work!
> > > > > > >
> > > > > > > I really like how gtest helps make the actual tests to be easier to
> > > > > > > read.
> > > > > > >
> > > > > > > On 2021-05-03 16:33:07 -0300, Nícolas F. R. A. Prado wrote:
> > > > > > > > Refactor lc-compliance using Googletest as the test framework.
> > > > > > > > 
> > > > > > > > Signed-off-by: Nícolas F. R. A. Prado <nfraprado at collabora.com>
> > > > > > > > ---
> > > > > > > > Changes in v2:
> > > > > > > > - Changed from static to dynamic test registration
> > > > > > > > - Removed -c flag
> > > > > > > > 
> > > > > > > > There's still an issue with the refactoring that the shared_ptr of the tests
> > > > > > > > apparently aren't being deleted on time, which causes:
> > > > > > > > 
> > > > > > > > 	[20:26:08.744507935] [103243] ERROR DeviceEnumerator device_enumerator.cpp:165 Removing media device /dev/media0 while still in use
> > > > > > > > 	[20:26:08.744548175] [103243] ERROR DeviceEnumerator device_enumerator.cpp:165 Removing media device /dev/media1 while still in use
> > > > > > > > 	Segmentation fault (core dumped)
> > > > > > > > 
> > > > > > > > I tried explicitly resetting the shared_ptr on the destructor of the tests but
> > > > > > > > that didn't work. In fact, when just listing the tests, the constructor for the
> > > > > > > > tests isn't even called. So I think the issue has to do with the passing of the
> > > > > > > > camera shared pointer to the REGISTER_TESTS() macro through that functor (?):
> > > > > > > > 
> > > > > > > > 	[=]() -> testsuite* { return new testcase(c, r, rq); }); \
> > > > > > > > 
> > > > > > > > Not sure how to solve this. Any tip would be welcome.
> > > > > > >
> > > > > > > I will leave this for now as I have a different worry about
> > > > > > > REGISTER_TESTS() design below.
> > > > > > >
> > > > > > > > 
> > > > > > > >  src/lc-compliance/main.cpp           |  99 +++++++++---------
> > > > > > > >  src/lc-compliance/meson.build        |   3 +
> > > > > > > >  src/lc-compliance/simple_capture.cpp |  74 +++++--------
> > > > > > > >  src/lc-compliance/simple_capture.h   |   8 +-
> > > > > > > >  src/lc-compliance/single_stream.cpp  | 151 ++++++++++++++-------------
> > > > > > > >  src/lc-compliance/tests.h            |  15 ++-
> > > > > > > >  6 files changed, 175 insertions(+), 175 deletions(-)
> > > > > > > > 
> > > > > > > > diff --git a/src/lc-compliance/main.cpp b/src/lc-compliance/main.cpp
> > > > > > > > index 54cee54aa978..add0d7729aec 100644
> > > > > > > > --- a/src/lc-compliance/main.cpp
> > > > > > > > +++ b/src/lc-compliance/main.cpp
> > > > > > > > @@ -9,6 +9,8 @@
> > > > > > > >  #include <iostream>
> > > > > > > >  #include <string.h>
> > > > > > > >  
> > > > > > > > +#include <gtest/gtest.h>
> > > > > > > > +
> > > > > > > >  #include <libcamera/libcamera.h>
> > > > > > > >  
> > > > > > > >  #include "../cam/options.h"
> > > > > > > > @@ -17,7 +19,6 @@
> > > > > > > >  using namespace libcamera;
> > > > > > > >  
> > > > > > > >  enum {
> > > > > > > > -	OptCamera = 'c',
> > > > > > > >  	OptHelp = 'h',
> > > > > > > >  };
> > > > > > > >  
> > > > > > > > @@ -28,14 +29,15 @@ public:
> > > > > > > >  	~Harness();
> > > > > > > >  
> > > > > > > >  	int exec();
> > > > > > > > +	int init();
> > > > > > > > +	void registerTests();
> > > > > > > >  
> > > > > > > >  private:
> > > > > > > > -	int init();
> > > > > > > >  	void listCameras();
> > > > > > > >  
> > > > > > > >  	OptionsParser::Options options_;
> > > > > > > >  	std::unique_ptr<CameraManager> cm_;
> > > > > > > > -	std::shared_ptr<Camera> camera_;
> > > > > > > > +	std::vector<std::shared_ptr<Camera>> cameras_;
> > > > > > > >  };
> > > > > > > >  
> > > > > > > >  Harness::Harness(const OptionsParser::Options &options)
> > > > > > > > @@ -46,33 +48,14 @@ Harness::Harness(const OptionsParser::Options &options)
> > > > > > > >  
> > > > > > > >  Harness::~Harness()
> > > > > > > >  {
> > > > > > > > -	if (camera_) {
> > > > > > > > -		camera_->release();
> > > > > > > > -		camera_.reset();
> > > > > > > > +	for (auto &c : cameras_) {
> > > > > > > > +		c->release();
> > > > > > > > +		c.reset();
> > > > > > > >  	}
> > > > > > > >  
> > > > > > > >  	cm_->stop();
> > > > > > > >  }
> > > > > > > >  
> > > > > > > > -int Harness::exec()
> > > > > > > > -{
> > > > > > > > -	int ret = init();
> > > > > > > > -	if (ret)
> > > > > > > > -		return ret;
> > > > > > > > -
> > > > > > > > -	std::vector<Results> results;
> > > > > > > > -
> > > > > > > > -	results.push_back(testSingleStream(camera_));
> > > > > > > > -
> > > > > > > > -	for (const Results &result : results) {
> > > > > > > > -		ret = result.summary();
> > > > > > > > -		if (ret)
> > > > > > > > -			return ret;
> > > > > > > > -	}
> > > > > > > > -
> > > > > > > > -	return 0;
> > > > > > > > -}
> > > > > > > > -
> > > > > > > >  int Harness::init()
> > > > > > > >  {
> > > > > > > >  	int ret = cm_->start();
> > > > > > > > @@ -82,42 +65,26 @@ int Harness::init()
> > > > > > > >  		return ret;
> > > > > > > >  	}
> > > > > > > >  
> > > > > > > > -	if (!options_.isSet(OptCamera)) {
> > > > > > > > -		std::cout << "No camera specified, available cameras:" << std::endl;
> > > > > > > > -		listCameras();
> > > > > > > > -		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();
> > > > > > > > -		return -ENODEV;
> > > > > > > > +	for (auto cam : cm_->cameras()) {
> > > > > > > > +		if (cam->acquire()) {
> > > > > > > > +			std::cout << "Failed to acquire camera" << std::endl;
> > > > > > > > +			return -EINVAL;
> > > > > > > > +		}
> > > > > > > > +		cameras_.push_back(cam);
> > > > > > >
> > > > > > > I don't like this, I think we need to retain the ability to select which
> > > > > > > camera to test. And further down the line maybe allow more then one
> > > > > > > camera to be selected if we want to test concurrent use of two specific
> > > > > > > ones. But for the work in this series I think you can ignore multiple
> > > > > > > cameras.
> > > > > > 
> > > > > > Actually, that doesn't mean that multiple cameras will be tested, that's
> > > > > > determined by the test filter, so doing --gtest_filter='.*idCamera1.*' would
> > > > > > only test that camera. But I think I see what you mean, I shouldn't be acquiring
> > > > > > all cameras, only the ones that I will actually test (and in this code there's a
> > > > > > big issue where if a single camera is already busy, even if it won't be tested,
> > > > > > lc-compliance as a whole fails).
> > > > >
> > > > > Ahh I understand.
> > > > >
> > > > > > 
> > > > > > >
> > > > > > > If I understand the coverletter correctly the correct thing here the cli
> > > > > > > arguments should be feed to gtest directly to be able to filter on what
> > > > > > > tests are run. Is there support in getst to also pass arguments to
> > > > > > > tests? Maybe we could remove the need to parse arguments ourself
> > > > > > > completely?
> > > > > > 
> > > > > > There isn't any CLI argument in gtest to pass arguments to tests, but I think we
> > > > > > could solve both problems (this and the one above) like this:
> > > > > > 
> > > > > > - In the main(), query the id of each camera from libcamera.
> > > > > > - Register each of the tests dynamically in gtest passing as an argument the
> > > > > >   camera id, rather than a camera pointer (which is the current behavior).
> > > > > > - To select which tests to run, and on which camera(s), the user just uses
> > > > > >   --gtest_filter='testToRun.*idCamera' (or '.*idCamera.*' to run all tests for
> > > > > >   the camera).
> > > > > > - In the startup of each test the camera is acquired for that test, and when
> > > > > >   it's done it is released.
> > > > > > 
> > > > > > Implications:
> > > > > > - We no longer need to parse arguments and can drop that code, as both camera
> > > > > >   and test are specified directly with gtest's filter.
> > > > > > - We only acquire the cameras that we'll actually test and only while they're
> > > > > >   being tested.
> > > > > > - As a downside, a single camera will probably be acquired multiple times during
> > > > > >   a single run as lc-compliance moves from one test to the next, but I don't
> > > > > >   think this would be a big issue. (Maybe we could do this at the test suite
> > > > > >   level instead to greatly decrease the number of times of acquire(), but I'm
> > > > > >   not sure if it's possible, since we instantiate, by passing the camera id, at
> > > > > >   the test level, I'll have to test that).
> > > > > > 
> > > > > > What do you think?
> > > > >
> > > > > I understand what you are trying to do but I see two issues.
> > > > >
> > > > > 1. It depends on the implicit knowledge of users how to build the filter
> > > > > string and is rather unintuitive compared to having two distinct
> > > > > arguments, one to select camera and one to express the test filter.
> > > > >
> > > > > This could possibly be worked around by keeping the cam options code
> > > > > and building the gtest filter string internally by injecitng the
> > > > > selected camera name in the correct location, but the we have the
> > > > > next issue,
> > > > >
> > > > > 2. I don't think it will be practically possible to express a test
> > > > > filter that selects a specific combination of two cameras.
> > > > >
> > > > > I do think we need to be able to parameterize test cases. If gtest does
> > > > > not allow to do so using function arguments there are other ways of
> > > > > doing so.
> > > > 
> > > > I think I didn't express myself well. I meant to say that gtest's CLI doesn't
> > > > have a flag to allow passing parameters to tests, meaning that it would not be
> > > > possible to parametrize the tests from the command line and also remove our
> > > > own parser and rely on gtest's.
> > >
> > > Check, I think we need to keep our own parser for now as we need to be
> > > able to parametrize the tests from the CLI.
> > >
> > > > But gtest does allow test parametrization at runtime and I'm already relying on
> > > > it since v2. Let me go a bit more over the way I'm doing this.
> > > > 
> > > > - I have a test defined, eg BalancedSingleCycle, which takes some parameters:
> > > > 
> > > > 	std::shared_ptr<Camera> camera, StreamRole role, unsigned int numRequests
> > > > 
> > > > - Since we may want to run the same test for all cameras, roles and numRequests,
> > > >   I register it, at runtime, using my REGISTER_TESTS() macro, which calls
> > > >   gtest's testing::RegisterTest() with all possible combinations of parameters.
> > > > 
> > > > - Based on the --gtest_filter CLI argument from gtest, it decides which tests
> > > >   are actually run
> > > > 
> > > > So the key point is that even though I parametrize the tests, I do it for all
> > > > combinations in order to later rely on gtest_filter to select which tests to
> > > > run.
> > >
> > > I see, that explains how the parameters get filled. I noticed it but did
> > > not dig further, see below for an expansion of my thoughts now that I
> > > understand this detail :-)
> > >
> > > > 
> > > > Now, I do agree with both of your points. And if we want to make the interface
> > > > more intuitive to use (which I also find important), there's no way around
> > > > having our own parser.
> > > > 
> > > > And if we're using our own camera flag, then it makes no sense to register tests
> > > > for all cameras, we can register tests only with the specified camera.
> > >
> > > This feels wrong. It feels like tests should be register without a
> > > camera component. The camera (and possible other arguments) should come
> > > from either arguments [1] or a singleton parameter class [2].
> > >
> > > 1. This do not seem to be possible with gtests as test parameters needs
> > > to be registerd.
> > >
> > > 2. This would be similar to the global variable used in earlier versions
> > > but instead modeled as a class singleton like CameraManager.
> > >
> > > Maybe there are other options tho.
> > >
> > > > 
> > > > I think here it would be helpful to think if the interface suits our problem.
> > > > To me we're looking at having:
> > > > 
> > > > 	lc-compliance --camera 'cameraId' --filter 'BalancedSingleCycle'
> > >
> > > This matches my view as well.
> > >
> > > > 
> > > > to only test camera 'cameraId' with a single test 'BalancedSingleCycle'. Here,
> > > > the camera flag is used to only register tests with that camera, and the filter
> > > > is passed as --gtest_filter, possibly a bit adapted, to gtest.
> > > > 
> > > > And if we imagine we have a test ConcurrentCameras, which receives two cameras
> > > > as parameters, and we call
> > > > 
> > > > 	lc-compliance --camera 'cameraId1,cameraId2'
> > >
> > > This also looks good. But for now lets focus on one camera, I think it
> > > will give us enough headache :-)
> > >
> > > > 
> > > > it would test both each of the cameras separately in all current tests which
> > > > accept a single camera, like BalancedSingleCycle, as well as test both at the
> > > > same time in the ConcurrentCameras test (and we could make only the former or
> > > > the latter occur by using the filter parameter).
> > >
> > > I have not thought enough about this to know if that is a good idea or
> > > not. At the top of my head I still tend to think that lc-compliance
> > > tests a single 'primary' camera and any additional camera supplied are
> > > only used for tests where two (or more) cameras are needed to test some
> > > specific feature of the primary camera.
> > >
> > > That is only the 'primary' camera is testes, the other camera(s) are
> > > just there to help/provoke the testing. But maybe I'm to narrow minded.
> >
> > Okay, so it now comes down to how lc-compliance will get used. Either:
> >
> > 1. lc-compliance will be called to test a single camera, with optionally other
> > secondary cameras
> >
> > 2. lc-compliance will be called to test multiple cameras at once (not
> > necessarily simultaneously, but in a single run), with the default case being
> > "Test all the cameras in this system".
> >
> > Given that the final objective of my internship is to have lc-compliance running
> > on real boards by KernelCI, to catch any kernel regressions that affect camera
> > usage, I'm trying to think how it would work there. (I wonder if this discussion
> > should be happening together with KernelCI to decide on this...)
> >
> > So I'm imagining a RockPi4 board with a camera sensor plugged in, running
> > lc-compliance to catch any kernel regressions.
> >
> > The limitation I see in 1 is that if a system has multiple cameras and want to
> > ensure that all keep working, it will need to either hardcode all camera ids and
> > manually call lc-compliance once for each one, or have a separate program that
> > lists all cameras from libcamera (using 'cam -l' for example) and calls
> > lc-compliance with each one. That's to say, running on all cameras is not as
> > easy. And then there are multiple reports, one for each camera, instead of a
> > single one for all cameras, which might be good or bad, not sure.
> >
> > That said, since the RockPi4 (and also the Raspberry Pi 4) has a single CSI-2
> > connector, I believe it only supports a single camera anyway, so this shouldn't
> > be a problem for this board, but I'm not sure about others. Phones for example
> > seem to have at least two independent cameras...
> 
> So, I just read about the Singleton pattern on Wikipedia and checked out the
> CameraManager class (should've done it before replying :) ).
> 
> From what I can tell, there's a big difference between the CameraManager class
> and a Singleton. The CameraManager just makes sure that there's a single
> instance of it by saving a reference to it in an internal static variable. A
> Singleton apparently, in addition to doing that, makes its instatiation through
> a public static function. This means that not only there's a single instance,
> it's also available from the global scope.

You are correct, my bad. I thought CameraManager implemented the 
Singleton pattern.

> 
> So I wanted to ask you to clarify which one you meant.

Always a good idea as I easily get lost ;-)

> If you meant that we
> should have a class like CameraManager, which just makes sure it's the single
> instance, my previous comments below are still valid.
> 
> On the other hand, if you meant that we should a have singleton, which is
> available globally, then it is basically the same thing as having a global
> variable, only a bit more organized, and my comments below are no longer valid.
> We could indeed go back to static test registration, since the camera instance
> could be accessed from any test as something like
> 
> 	TestEnvironment::getInstance().camera()

This is what I meant, a more organized way to reach the global variable 
to allow for static test registration while still being able to 
parametrize the tests. Sorry for the confusion by bringing up 
CameraManager.

> 
> making the alternative 1 have the benefit of making the test registration and
> definition code simpler.
> 
> Thanks,
> Nícolas
> 
> >
> > Regarding the benefit of 1, I'm not sure. If we were to use a global variable
> > like in v1, it would make the code a lot simpler by making the test registration
> > static. But that doesn't seem to be an option :). I'm not familiar with using a
> > Singleton, so I'll take a look at CameraManager. But I assume it wouldn't be
> > accessible statically, so the dynamic test registration complexities would have
> > to stay.
> >
> > Also, just to make sure we're on the same page, by using a Singleton, and again
> > assuming it isn't stored in a global variable to be accessible from the tests
> > statically, even though we would eliminate the camera as a parameter for the
> > tests, the reference to the singleton itself would need to be added as a
> > parameter for the tests.
> >
> > Thanks,
> > Nícolas
> >
> > >
> > > > 
> > > > I know we don't need to implement support for multiple cameras right now, I just
> > > > want to make sure it's possible and thinking about the interface and the
> > > > scenarios help me picture how they'd work.
> > > > 
> > > > Thoughts?
> > > > 
> > > > Thanks,
> > > > Nícolas
> > > > 
> > > > >
> > > > > I'm sure there are many solutions to this problem and maybe someone more
> > > > > familiar with gtest knows of a neat way to piggyback on its
> > > > > infrastrcuture? Since I know very little about the gtest framework the
> > > > > following is just an idea and I'm happy to yield.
> > > > >
> > > > > How about using a singleton class that carries arguments constructed
> > > > > from cli arguments (camera id) before the gtest are executed. Each test
> > > > > case could then consult this singleton if it needs to access parameters.
> > > > > I think you could take inspiration from the libcamera CameraManager
> > > > > class.
> > > > >
> > > > > This could (at lest for now) imply keeping the cam option parser. If so
> > > > > I think it would be nice if somewhen before we integrate this work that
> > > > > the gtest filter string could be supplied as an argument using that
> > > > > parser. Of course if we can do it the other way around and add arguments
> > > > > to the gtest parser that would be even cooler as it would be less code
> > > > > for us to maintain ;-)

-- 
Regards,
Niklas Söderlund


More information about the libcamera-devel mailing list