[{"id":22865,"web_url":"https://patchwork.libcamera.org/comment/22865/","msgid":"<YnQO+51KeS06rDc9@pendragon.ideasonboard.com>","date":"2022-05-05T17:52:59","subject":"Re: [libcamera-devel] [PATCH v7 06/13] py: add unittests.py","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Tomi,\n\nThank you for the patch.\n\nOn Thu, May 05, 2022 at 01:40:57PM +0300, Tomi Valkeinen wrote:\n> Add a simple unittests.py as a base for python unittests.\n> \n> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> ---\n>  test/meson.build     |   1 +\n>  test/py/meson.build  |  17 ++\n>  test/py/unittests.py | 368 +++++++++++++++++++++++++++++++++++++++++++\n>  3 files changed, 386 insertions(+)\n>  create mode 100644 test/py/meson.build\n>  create mode 100755 test/py/unittests.py\n> \n> diff --git a/test/meson.build b/test/meson.build\n> index fd4c5ca0..623f3baa 100644\n> --- a/test/meson.build\n> +++ b/test/meson.build\n> @@ -18,6 +18,7 @@ subdir('log')\n>  subdir('media_device')\n>  subdir('pipeline')\n>  subdir('process')\n> +subdir('py')\n>  subdir('serialization')\n>  subdir('stream')\n>  subdir('v4l2_compat')\n> diff --git a/test/py/meson.build b/test/py/meson.build\n> new file mode 100644\n> index 00000000..f6b42bd0\n> --- /dev/null\n> +++ b/test/py/meson.build\n> @@ -0,0 +1,17 @@\n> +# SPDX-License-Identifier: CC0-1.0\n> +\n> +if not pycamera_enabled\n> +    subdir_done()\n> +endif\n> +\n> +pymod = import('python')\n> +py3 = pymod.find_installation('python3')\n> +\n> +pypathdir = meson.project_build_root() / 'src/py'\n\npypathdir = meson.project_build_root() / 'src' / 'py'\n\n> +\n> +test('pyunittests',\n> +     py3,\n> +     args : files('unittests.py'),\n> +     env : ['PYTHONPATH=' + pypathdir],\n> +     suite : 'pybindings',\n> +     is_parallel : false)\n> diff --git a/test/py/unittests.py b/test/py/unittests.py\n> new file mode 100755\n> index 00000000..15d5b4a7\n> --- /dev/null\n> +++ b/test/py/unittests.py\n> @@ -0,0 +1,368 @@\n> +#!/usr/bin/env python3\n> +\n> +# SPDX-License-Identifier: GPL-2.0-or-later\n> +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> +\n> +from collections import defaultdict\n> +import errno\n> +import gc\n> +import libcamera as libcam\n\nI'm tempted to claim the \"cam\" name in all our Python code, or possibly\n\"camera\". What do you think ?\n\n> +import os\n> +import selectors\n> +import time\n> +import unittest\n> +import weakref\n> +\n> +\n> +class MyTestCase(unittest.TestCase):\n\ns/MyTestCase/BaseTestCase/\n\nIt's not yours only anymore :-)\n\n> +    def assertZero(self, a, msg=None):\n> +        self.assertEqual(a, 0, msg)\n> +\n> +\n> +class SimpleTestMethods(MyTestCase):\n> +    def test_find_ref(self):\n> +        cm = libcam.CameraManager.singleton()\n> +        wr_cm = weakref.ref(cm)\n> +\n> +        cam = cm.find(\"platform/vimc\")\n\nStandardizing on single-quotes here too would be nice.\n\n> +        self.assertIsNotNone(cam)\n> +        wr_cam = weakref.ref(cam)\n> +\n> +        cm = None\n> +        gc.collect()\n> +        self.assertIsNotNone(wr_cm())\n> +\n> +        cam = None\n> +        gc.collect()\n> +        self.assertIsNone(wr_cm())\n> +        self.assertIsNone(wr_cam())\n> +\n> +    def test_get_ref(self):\n> +        cm = libcam.CameraManager.singleton()\n> +        wr_cm = weakref.ref(cm)\n> +\n> +        cam = cm.get(\"platform/vimc.0 Sensor B\")\n> +        self.assertTrue(cam is not None)\n> +        wr_cam = weakref.ref(cam)\n> +\n> +        cm = None\n> +        gc.collect()\n> +        self.assertIsNotNone(wr_cm())\n> +\n> +        cam = None\n> +        gc.collect()\n> +        self.assertIsNone(wr_cm())\n> +        self.assertIsNone(wr_cam())\n> +\n> +    def test_acquire_release(self):\n> +        cm = libcam.CameraManager.singleton()\n> +        cam = cm.get(\"platform/vimc.0 Sensor B\")\n> +        self.assertTrue(cam is not None)\n> +\n> +        ret = cam.acquire()\n> +        self.assertZero(ret)\n> +\n> +        ret = cam.release()\n> +        self.assertZero(ret)\n> +\n> +    def test_double_acquire(self):\n> +        cm = libcam.CameraManager.singleton()\n> +        cam = cm.get(\"platform/vimc.0 Sensor B\")\n> +        self.assertTrue(cam is not None)\n> +\n> +        ret = cam.acquire()\n> +        self.assertZero(ret)\n> +\n> +        libcam.logSetLevel(\"Camera\", \"FATAL\")\n> +        ret = cam.acquire()\n> +        self.assertEqual(ret, -errno.EBUSY)\n> +        libcam.logSetLevel(\"Camera\", \"ERROR\")\n> +\n> +        ret = cam.release()\n> +        self.assertZero(ret)\n> +\n> +        ret = cam.release()\n> +        # I expected EBUSY, but looks like double release works fine\n> +        self.assertZero(ret)\n> +\n> +\n> +class CameraTesterBase(MyTestCase):\n> +    def setUp(self):\n> +        self.cm = libcam.CameraManager.singleton()\n> +        self.cam = self.cm.find(\"platform/vimc\")\n> +        if self.cam is None:\n> +            self.cm = None\n> +            raise Exception(\"No vimc found\")\n\nIs it possible to skip the test in that case instead of failing ? This\ncould be done on top, a todo comment somewhere in the file would be good\nthen.\n\n> +\n> +        ret = self.cam.acquire()\n> +        if ret != 0:\n> +            self.cam = None\n> +            self.cm = None\n> +            raise Exception(\"Failed to acquire camera\")\n> +\n> +    def tearDown(self):\n> +        # If a test fails, the camera may be in running state. So always stop.\n> +        self.cam.stop()\n> +\n> +        ret = self.cam.release()\n> +        if ret != 0:\n> +            raise Exception(\"Failed to release camera\")\n> +\n> +        self.cam = None\n> +        self.cm = None\n> +\n> +\n> +class AllocatorTestMethods(CameraTesterBase):\n> +    def test_allocator(self):\n> +        cam = self.cam\n> +\n> +        camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture])\n> +        self.assertTrue(camconfig.size == 1)\n> +        wr_camconfig = weakref.ref(camconfig)\n> +\n> +        streamconfig = camconfig.at(0)\n> +        wr_streamconfig = weakref.ref(streamconfig)\n> +\n> +        ret = cam.configure(camconfig)\n> +        self.assertZero(ret)\n> +\n> +        stream = streamconfig.stream\n> +        wr_stream = weakref.ref(stream)\n> +\n> +        # stream should keep streamconfig and camconfig alive\n> +        streamconfig = None\n> +        camconfig = None\n> +        gc.collect()\n> +        self.assertIsNotNone(wr_camconfig())\n> +        self.assertIsNotNone(wr_streamconfig())\n> +\n> +        allocator = libcam.FrameBufferAllocator(cam)\n> +        ret = allocator.allocate(stream)\n> +        self.assertTrue(ret > 0)\n> +        wr_allocator = weakref.ref(allocator)\n> +\n> +        buffers = allocator.buffers(stream)\n> +        buffers = None\n> +\n> +        buffer = allocator.buffers(stream)[0]\n> +        self.assertIsNotNone(buffer)\n> +        wr_buffer = weakref.ref(buffer)\n> +\n> +        allocator = None\n> +        gc.collect()\n> +        self.assertIsNotNone(wr_buffer())\n> +        self.assertIsNotNone(wr_allocator())\n> +        self.assertIsNotNone(wr_stream())\n> +\n> +        buffer = None\n> +        gc.collect()\n> +        self.assertIsNone(wr_buffer())\n> +        self.assertIsNone(wr_allocator())\n> +        self.assertIsNotNone(wr_stream())\n> +\n> +        stream = None\n> +        gc.collect()\n> +        self.assertIsNone(wr_stream())\n> +        self.assertIsNone(wr_camconfig())\n> +        self.assertIsNone(wr_streamconfig())\n> +\n> +\n> +class SimpleCaptureMethods(CameraTesterBase):\n> +    def test_sleep(self):\n> +        cm = self.cm\n> +        cam = self.cam\n> +\n> +        camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture])\n> +        self.assertTrue(camconfig.size == 1)\n> +\n> +        streamconfig = camconfig.at(0)\n> +        fmts = streamconfig.formats\n> +\n> +        ret = cam.configure(camconfig)\n> +        self.assertZero(ret)\n> +\n> +        stream = streamconfig.stream\n> +\n> +        allocator = libcam.FrameBufferAllocator(cam)\n> +        ret = allocator.allocate(stream)\n> +        self.assertTrue(ret > 0)\n> +\n> +        num_bufs = len(allocator.buffers(stream))\n> +\n> +        reqs = []\n> +        for i in range(num_bufs):\n> +            req = cam.createRequest(i)\n> +            self.assertIsNotNone(req)\n> +\n> +            buffer = allocator.buffers(stream)[i]\n> +            ret = req.addBuffer(stream, buffer)\n> +            self.assertZero(ret)\n> +\n> +            reqs.append(req)\n> +\n> +        buffer = None\n> +\n> +        ret = cam.start()\n> +        self.assertZero(ret)\n> +\n> +        for req in reqs:\n> +            ret = cam.queueRequest(req)\n> +            self.assertZero(ret)\n> +\n> +        reqs = None\n> +        gc.collect()\n> +\n> +        time.sleep(0.5)\n> +\n> +        reqs = cm.getReadyRequests()\n> +\n> +        self.assertTrue(len(reqs) == num_bufs)\n> +\n> +        for i, req in enumerate(reqs):\n> +            self.assertTrue(i == req.cookie)\n> +\n> +        reqs = None\n> +        gc.collect()\n> +\n> +        ret = cam.stop()\n> +        self.assertZero(ret)\n> +\n> +    def test_select(self):\n> +        cm = self.cm\n> +        cam = self.cam\n> +\n> +        camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture])\n> +        self.assertTrue(camconfig.size == 1)\n> +\n> +        streamconfig = camconfig.at(0)\n> +        fmts = streamconfig.formats\n> +\n> +        ret = cam.configure(camconfig)\n> +        self.assertZero(ret)\n> +\n> +        stream = streamconfig.stream\n> +\n> +        allocator = libcam.FrameBufferAllocator(cam)\n> +        ret = allocator.allocate(stream)\n> +        self.assertTrue(ret > 0)\n> +\n> +        num_bufs = len(allocator.buffers(stream))\n> +\n> +        reqs = []\n> +        for i in range(num_bufs):\n> +            req = cam.createRequest(i)\n> +            self.assertIsNotNone(req)\n> +\n> +            buffer = allocator.buffers(stream)[i]\n> +            ret = req.addBuffer(stream, buffer)\n> +            self.assertZero(ret)\n> +\n> +            reqs.append(req)\n> +\n> +        buffer = None\n> +\n> +        ret = cam.start()\n> +        self.assertZero(ret)\n> +\n> +        for req in reqs:\n> +            ret = cam.queueRequest(req)\n> +            self.assertZero(ret)\n> +\n> +        reqs = None\n> +        gc.collect()\n> +\n> +        sel = selectors.DefaultSelector()\n> +        sel.register(cm.efd, selectors.EVENT_READ, 123)\n\nIs the data argument needed ?\n\n> +\n> +        reqs = []\n> +\n> +        running = True\n> +        while running:\n> +            events = sel.select()\n> +            for key, mask in events:\n> +                os.read(key.fileobj, 8)\n> +\n> +                ready_reqs = cm.getReadyRequests()\n> +\n> +                self.assertTrue(len(ready_reqs) > 0)\n> +\n> +                reqs += ready_reqs\n> +\n> +                if len(reqs) == num_bufs:\n> +                    running = False\n> +\n> +        self.assertTrue(len(reqs) == num_bufs)\n> +\n> +        for i, req in enumerate(reqs):\n> +            self.assertTrue(i == req.cookie)\n> +\n> +        reqs = None\n> +        gc.collect()\n> +\n> +        ret = cam.stop()\n> +        self.assertZero(ret)\n> +\n> +\n> +# Recursively expand slist's objects into olist, using seen to track already\n> +# processed objects.\n> +def _getr(slist, olist, seen):\n> +    for e in slist:\n> +        if id(e) in seen:\n> +            continue\n> +        seen.add(id(e))\n> +        olist.append(e)\n> +        tl = gc.get_referents(e)\n> +        if tl:\n> +            _getr(tl, olist, seen)\n> +\n> +\n> +def get_all_objects(ignored=[]):\n> +    gcl = gc.get_objects()\n> +    olist = []\n> +    seen = set()\n> +\n> +    seen.add(id(gcl))\n> +    seen.add(id(olist))\n> +    seen.add(id(seen))\n> +    seen.update(set([id(o) for o in ignored]))\n> +\n> +    _getr(gcl, olist, seen)\n> +\n> +    return olist\n> +\n> +\n> +def create_type_count_map(olist):\n> +    map = defaultdict(int)\n> +    for o in olist:\n> +        map[type(o)] += 1\n> +    return map\n> +\n> +\n> +def diff_type_count_maps(before, after):\n> +    return [(k, after[k] - before[k]) for k in after if after[k] != before[k]]\n> +\n> +\n> +if __name__ == '__main__':\n> +    # doesn't work very well, as things always leak a bit\n\nLovely :-) Is it something we can fix ?\n\ns/doesn't/Doesn't/\n\nReviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n\n> +    test_leaks = False\n> +\n> +    if test_leaks:\n> +        gc.unfreeze()\n> +        gc.collect()\n> +\n> +        obs_before = get_all_objects()\n> +\n> +    unittest.main(exit=False)\n> +\n> +    if test_leaks:\n> +        gc.unfreeze()\n> +        gc.collect()\n> +\n> +        obs_after = get_all_objects([obs_before])\n> +\n> +        before = create_type_count_map(obs_before)\n> +        after = create_type_count_map(obs_after)\n> +\n> +        leaks = diff_type_count_maps(before, after)\n> +        if len(leaks) > 0:\n> +            print(leaks)","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 40CA4C3256\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu,  5 May 2022 17:53:06 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 90CC460424;\n\tThu,  5 May 2022 19:53:05 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id E0A50603AB\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu,  5 May 2022 19:53:03 +0200 (CEST)","from pendragon.ideasonboard.com (62-78-145-57.bb.dnainternet.fi\n\t[62.78.145.57])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 24739492;\n\tThu,  5 May 2022 19:53:03 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1651773185;\n\tbh=IOSrUG3HWC2HkyRASZTbU+H+eGFiHnKDWJ+W15ZrRKA=;\n\th=Date:To:References:In-Reply-To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=UaENZSPtf/Fngl11CQJWemIUvJ9yvxUFhl422W0L1TBjPtZatOR0wR758ddeM4DWw\n\tA4ZCukqunTvU9cvInUuzlt2HArQvAMcwcjACX00QC5ofFzwOpUwhPOD7UNDLIQ2M4S\n\tRaOVNyAH1ZapTmgWau+2qYCLwUNvI6Suddami1yuQNwFSU1K/o0yl0P1F8obmaeYCX\n\tp20sYJ3/EGJmnKceaNVSTYNV8VE4UOEEOlll26pU6oAHRXkQIGDzihWLBsP0+lAW8P\n\tGqO/b2Jvy1Bmhr93HsXmhGztMcTRsK0FXqoqAxrPX4Q1eoxwFQh2GMbBB6xvV04XyK\n\t/vyYsMAOV6Bkg==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1651773183;\n\tbh=IOSrUG3HWC2HkyRASZTbU+H+eGFiHnKDWJ+W15ZrRKA=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=IILERlDVheAYinFu6fVq+Kx4EviBmvHdjzl4ZiSjckFFFxNkLpcatMPiRGJ+JsLGc\n\tcIBJKOoxHND/pBOFuWM99h9AggN67/zpHQ3i3GglziVetrTZfKbPFwSWTJiPukEVv0\n\tOBdcI1gxVyeEAzsJuoeq/57mgNk4h7agsZA7zs2g="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"IILERlDV\"; dkim-atps=neutral","Date":"Thu, 5 May 2022 20:52:59 +0300","To":"Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Message-ID":"<YnQO+51KeS06rDc9@pendragon.ideasonboard.com>","References":"<20220505104104.70841-1-tomi.valkeinen@ideasonboard.com>\n\t<20220505104104.70841-7-tomi.valkeinen@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20220505104104.70841-7-tomi.valkeinen@ideasonboard.com>","Subject":"Re: [libcamera-devel] [PATCH v7 06/13] py: add unittests.py","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","From":"Laurent Pinchart via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":22875,"web_url":"https://patchwork.libcamera.org/comment/22875/","msgid":"<65d2c5d6-f70b-494d-015d-2d91b70fc60a@ideasonboard.com>","date":"2022-05-06T10:59:38","subject":"Re: [libcamera-devel] [PATCH v7 06/13] py: add unittests.py","submitter":{"id":109,"url":"https://patchwork.libcamera.org/api/people/109/","name":"Tomi Valkeinen","email":"tomi.valkeinen@ideasonboard.com"},"content":"On 05/05/2022 20:52, Laurent Pinchart wrote:\n> Hi Tomi,\n> \n> Thank you for the patch.\n> \n> On Thu, May 05, 2022 at 01:40:57PM +0300, Tomi Valkeinen wrote:\n>> Add a simple unittests.py as a base for python unittests.\n>>\n>> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>> ---\n>>   test/meson.build     |   1 +\n>>   test/py/meson.build  |  17 ++\n>>   test/py/unittests.py | 368 +++++++++++++++++++++++++++++++++++++++++++\n>>   3 files changed, 386 insertions(+)\n>>   create mode 100644 test/py/meson.build\n>>   create mode 100755 test/py/unittests.py\n>>\n>> diff --git a/test/meson.build b/test/meson.build\n>> index fd4c5ca0..623f3baa 100644\n>> --- a/test/meson.build\n>> +++ b/test/meson.build\n>> @@ -18,6 +18,7 @@ subdir('log')\n>>   subdir('media_device')\n>>   subdir('pipeline')\n>>   subdir('process')\n>> +subdir('py')\n>>   subdir('serialization')\n>>   subdir('stream')\n>>   subdir('v4l2_compat')\n>> diff --git a/test/py/meson.build b/test/py/meson.build\n>> new file mode 100644\n>> index 00000000..f6b42bd0\n>> --- /dev/null\n>> +++ b/test/py/meson.build\n>> @@ -0,0 +1,17 @@\n>> +# SPDX-License-Identifier: CC0-1.0\n>> +\n>> +if not pycamera_enabled\n>> +    subdir_done()\n>> +endif\n>> +\n>> +pymod = import('python')\n>> +py3 = pymod.find_installation('python3')\n>> +\n>> +pypathdir = meson.project_build_root() / 'src/py'\n> \n> pypathdir = meson.project_build_root() / 'src' / 'py'\n> \n>> +\n>> +test('pyunittests',\n>> +     py3,\n>> +     args : files('unittests.py'),\n>> +     env : ['PYTHONPATH=' + pypathdir],\n>> +     suite : 'pybindings',\n>> +     is_parallel : false)\n>> diff --git a/test/py/unittests.py b/test/py/unittests.py\n>> new file mode 100755\n>> index 00000000..15d5b4a7\n>> --- /dev/null\n>> +++ b/test/py/unittests.py\n>> @@ -0,0 +1,368 @@\n>> +#!/usr/bin/env python3\n>> +\n>> +# SPDX-License-Identifier: GPL-2.0-or-later\n>> +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>> +\n>> +from collections import defaultdict\n>> +import errno\n>> +import gc\n>> +import libcamera as libcam\n> \n> I'm tempted to claim the \"cam\" name in all our Python code, or possibly\n> \"camera\". What do you think ?\n\nI think 'cam' and 'camera' are names for variables that contain a camera.\n\n>> +import os\n>> +import selectors\n>> +import time\n>> +import unittest\n>> +import weakref\n>> +\n>> +\n>> +class MyTestCase(unittest.TestCase):\n> \n> s/MyTestCase/BaseTestCase/\n> \n> It's not yours only anymore :-)\n\nMy precious...\n\n>> +    def assertZero(self, a, msg=None):\n>> +        self.assertEqual(a, 0, msg)\n>> +\n>> +\n>> +class SimpleTestMethods(MyTestCase):\n>> +    def test_find_ref(self):\n>> +        cm = libcam.CameraManager.singleton()\n>> +        wr_cm = weakref.ref(cm)\n>> +\n>> +        cam = cm.find(\"platform/vimc\")\n> \n> Standardizing on single-quotes here too would be nice.\n\nI can't stand them, but... Fine, it's probably best to follow the widely \nused convention.\n\n>> +        self.assertIsNotNone(cam)\n>> +        wr_cam = weakref.ref(cam)\n>> +\n>> +        cm = None\n>> +        gc.collect()\n>> +        self.assertIsNotNone(wr_cm())\n>> +\n>> +        cam = None\n>> +        gc.collect()\n>> +        self.assertIsNone(wr_cm())\n>> +        self.assertIsNone(wr_cam())\n>> +\n>> +    def test_get_ref(self):\n>> +        cm = libcam.CameraManager.singleton()\n>> +        wr_cm = weakref.ref(cm)\n>> +\n>> +        cam = cm.get(\"platform/vimc.0 Sensor B\")\n>> +        self.assertTrue(cam is not None)\n>> +        wr_cam = weakref.ref(cam)\n>> +\n>> +        cm = None\n>> +        gc.collect()\n>> +        self.assertIsNotNone(wr_cm())\n>> +\n>> +        cam = None\n>> +        gc.collect()\n>> +        self.assertIsNone(wr_cm())\n>> +        self.assertIsNone(wr_cam())\n>> +\n>> +    def test_acquire_release(self):\n>> +        cm = libcam.CameraManager.singleton()\n>> +        cam = cm.get(\"platform/vimc.0 Sensor B\")\n>> +        self.assertTrue(cam is not None)\n>> +\n>> +        ret = cam.acquire()\n>> +        self.assertZero(ret)\n>> +\n>> +        ret = cam.release()\n>> +        self.assertZero(ret)\n>> +\n>> +    def test_double_acquire(self):\n>> +        cm = libcam.CameraManager.singleton()\n>> +        cam = cm.get(\"platform/vimc.0 Sensor B\")\n>> +        self.assertTrue(cam is not None)\n>> +\n>> +        ret = cam.acquire()\n>> +        self.assertZero(ret)\n>> +\n>> +        libcam.logSetLevel(\"Camera\", \"FATAL\")\n>> +        ret = cam.acquire()\n>> +        self.assertEqual(ret, -errno.EBUSY)\n>> +        libcam.logSetLevel(\"Camera\", \"ERROR\")\n>> +\n>> +        ret = cam.release()\n>> +        self.assertZero(ret)\n>> +\n>> +        ret = cam.release()\n>> +        # I expected EBUSY, but looks like double release works fine\n>> +        self.assertZero(ret)\n>> +\n>> +\n>> +class CameraTesterBase(MyTestCase):\n>> +    def setUp(self):\n>> +        self.cm = libcam.CameraManager.singleton()\n>> +        self.cam = self.cm.find(\"platform/vimc\")\n>> +        if self.cam is None:\n>> +            self.cm = None\n>> +            raise Exception(\"No vimc found\")\n> \n> Is it possible to skip the test in that case instead of failing ? This\n> could be done on top, a todo comment somewhere in the file would be good\n> then.\n\nYes, I can do that. Although some tests test finding the vimc camera, so \nthose will still fail.\n\n>> +\n>> +        ret = self.cam.acquire()\n>> +        if ret != 0:\n>> +            self.cam = None\n>> +            self.cm = None\n>> +            raise Exception(\"Failed to acquire camera\")\n>> +\n>> +    def tearDown(self):\n>> +        # If a test fails, the camera may be in running state. So always stop.\n>> +        self.cam.stop()\n>> +\n>> +        ret = self.cam.release()\n>> +        if ret != 0:\n>> +            raise Exception(\"Failed to release camera\")\n>> +\n>> +        self.cam = None\n>> +        self.cm = None\n>> +\n>> +\n>> +class AllocatorTestMethods(CameraTesterBase):\n>> +    def test_allocator(self):\n>> +        cam = self.cam\n>> +\n>> +        camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture])\n>> +        self.assertTrue(camconfig.size == 1)\n>> +        wr_camconfig = weakref.ref(camconfig)\n>> +\n>> +        streamconfig = camconfig.at(0)\n>> +        wr_streamconfig = weakref.ref(streamconfig)\n>> +\n>> +        ret = cam.configure(camconfig)\n>> +        self.assertZero(ret)\n>> +\n>> +        stream = streamconfig.stream\n>> +        wr_stream = weakref.ref(stream)\n>> +\n>> +        # stream should keep streamconfig and camconfig alive\n>> +        streamconfig = None\n>> +        camconfig = None\n>> +        gc.collect()\n>> +        self.assertIsNotNone(wr_camconfig())\n>> +        self.assertIsNotNone(wr_streamconfig())\n>> +\n>> +        allocator = libcam.FrameBufferAllocator(cam)\n>> +        ret = allocator.allocate(stream)\n>> +        self.assertTrue(ret > 0)\n>> +        wr_allocator = weakref.ref(allocator)\n>> +\n>> +        buffers = allocator.buffers(stream)\n>> +        buffers = None\n>> +\n>> +        buffer = allocator.buffers(stream)[0]\n>> +        self.assertIsNotNone(buffer)\n>> +        wr_buffer = weakref.ref(buffer)\n>> +\n>> +        allocator = None\n>> +        gc.collect()\n>> +        self.assertIsNotNone(wr_buffer())\n>> +        self.assertIsNotNone(wr_allocator())\n>> +        self.assertIsNotNone(wr_stream())\n>> +\n>> +        buffer = None\n>> +        gc.collect()\n>> +        self.assertIsNone(wr_buffer())\n>> +        self.assertIsNone(wr_allocator())\n>> +        self.assertIsNotNone(wr_stream())\n>> +\n>> +        stream = None\n>> +        gc.collect()\n>> +        self.assertIsNone(wr_stream())\n>> +        self.assertIsNone(wr_camconfig())\n>> +        self.assertIsNone(wr_streamconfig())\n>> +\n>> +\n>> +class SimpleCaptureMethods(CameraTesterBase):\n>> +    def test_sleep(self):\n>> +        cm = self.cm\n>> +        cam = self.cam\n>> +\n>> +        camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture])\n>> +        self.assertTrue(camconfig.size == 1)\n>> +\n>> +        streamconfig = camconfig.at(0)\n>> +        fmts = streamconfig.formats\n>> +\n>> +        ret = cam.configure(camconfig)\n>> +        self.assertZero(ret)\n>> +\n>> +        stream = streamconfig.stream\n>> +\n>> +        allocator = libcam.FrameBufferAllocator(cam)\n>> +        ret = allocator.allocate(stream)\n>> +        self.assertTrue(ret > 0)\n>> +\n>> +        num_bufs = len(allocator.buffers(stream))\n>> +\n>> +        reqs = []\n>> +        for i in range(num_bufs):\n>> +            req = cam.createRequest(i)\n>> +            self.assertIsNotNone(req)\n>> +\n>> +            buffer = allocator.buffers(stream)[i]\n>> +            ret = req.addBuffer(stream, buffer)\n>> +            self.assertZero(ret)\n>> +\n>> +            reqs.append(req)\n>> +\n>> +        buffer = None\n>> +\n>> +        ret = cam.start()\n>> +        self.assertZero(ret)\n>> +\n>> +        for req in reqs:\n>> +            ret = cam.queueRequest(req)\n>> +            self.assertZero(ret)\n>> +\n>> +        reqs = None\n>> +        gc.collect()\n>> +\n>> +        time.sleep(0.5)\n>> +\n>> +        reqs = cm.getReadyRequests()\n>> +\n>> +        self.assertTrue(len(reqs) == num_bufs)\n>> +\n>> +        for i, req in enumerate(reqs):\n>> +            self.assertTrue(i == req.cookie)\n>> +\n>> +        reqs = None\n>> +        gc.collect()\n>> +\n>> +        ret = cam.stop()\n>> +        self.assertZero(ret)\n>> +\n>> +    def test_select(self):\n>> +        cm = self.cm\n>> +        cam = self.cam\n>> +\n>> +        camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture])\n>> +        self.assertTrue(camconfig.size == 1)\n>> +\n>> +        streamconfig = camconfig.at(0)\n>> +        fmts = streamconfig.formats\n>> +\n>> +        ret = cam.configure(camconfig)\n>> +        self.assertZero(ret)\n>> +\n>> +        stream = streamconfig.stream\n>> +\n>> +        allocator = libcam.FrameBufferAllocator(cam)\n>> +        ret = allocator.allocate(stream)\n>> +        self.assertTrue(ret > 0)\n>> +\n>> +        num_bufs = len(allocator.buffers(stream))\n>> +\n>> +        reqs = []\n>> +        for i in range(num_bufs):\n>> +            req = cam.createRequest(i)\n>> +            self.assertIsNotNone(req)\n>> +\n>> +            buffer = allocator.buffers(stream)[i]\n>> +            ret = req.addBuffer(stream, buffer)\n>> +            self.assertZero(ret)\n>> +\n>> +            reqs.append(req)\n>> +\n>> +        buffer = None\n>> +\n>> +        ret = cam.start()\n>> +        self.assertZero(ret)\n>> +\n>> +        for req in reqs:\n>> +            ret = cam.queueRequest(req)\n>> +            self.assertZero(ret)\n>> +\n>> +        reqs = None\n>> +        gc.collect()\n>> +\n>> +        sel = selectors.DefaultSelector()\n>> +        sel.register(cm.efd, selectors.EVENT_READ, 123)\n> \n> Is the data argument needed ?\n\nNo, it's not.\n\n>> +\n>> +        reqs = []\n>> +\n>> +        running = True\n>> +        while running:\n>> +            events = sel.select()\n>> +            for key, mask in events:\n>> +                os.read(key.fileobj, 8)\n>> +\n>> +                ready_reqs = cm.getReadyRequests()\n>> +\n>> +                self.assertTrue(len(ready_reqs) > 0)\n>> +\n>> +                reqs += ready_reqs\n>> +\n>> +                if len(reqs) == num_bufs:\n>> +                    running = False\n>> +\n>> +        self.assertTrue(len(reqs) == num_bufs)\n>> +\n>> +        for i, req in enumerate(reqs):\n>> +            self.assertTrue(i == req.cookie)\n>> +\n>> +        reqs = None\n>> +        gc.collect()\n>> +\n>> +        ret = cam.stop()\n>> +        self.assertZero(ret)\n>> +\n>> +\n>> +# Recursively expand slist's objects into olist, using seen to track already\n>> +# processed objects.\n>> +def _getr(slist, olist, seen):\n>> +    for e in slist:\n>> +        if id(e) in seen:\n>> +            continue\n>> +        seen.add(id(e))\n>> +        olist.append(e)\n>> +        tl = gc.get_referents(e)\n>> +        if tl:\n>> +            _getr(tl, olist, seen)\n>> +\n>> +\n>> +def get_all_objects(ignored=[]):\n>> +    gcl = gc.get_objects()\n>> +    olist = []\n>> +    seen = set()\n>> +\n>> +    seen.add(id(gcl))\n>> +    seen.add(id(olist))\n>> +    seen.add(id(seen))\n>> +    seen.update(set([id(o) for o in ignored]))\n>> +\n>> +    _getr(gcl, olist, seen)\n>> +\n>> +    return olist\n>> +\n>> +\n>> +def create_type_count_map(olist):\n>> +    map = defaultdict(int)\n>> +    for o in olist:\n>> +        map[type(o)] += 1\n>> +    return map\n>> +\n>> +\n>> +def diff_type_count_maps(before, after):\n>> +    return [(k, after[k] - before[k]) for k in after if after[k] != before[k]]\n>> +\n>> +\n>> +if __name__ == '__main__':\n>> +    # doesn't work very well, as things always leak a bit\n> \n> Lovely :-) Is it something we can fix ?\n\nI have no idea if this method is a valid way to observe object leaks, \nso... no clue. I think it can still be used, but manually, comparing \nobject lists when doing some changes to the tests.\n\n  Tomi","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 7BAC2C3256\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  6 May 2022 10:59:44 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id C5DDC65646;\n\tFri,  6 May 2022 12:59:43 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 2D892604A3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  6 May 2022 12:59:42 +0200 (CEST)","from [192.168.1.111] (91-156-85-209.elisa-laajakaista.fi\n\t[91.156.85.209])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 42EB7487;\n\tFri,  6 May 2022 12:59:41 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1651834783;\n\tbh=nd6esZNhd2hGtFIOVGjJPgq3BC0eTt0J6CNl53h7/ZA=;\n\th=Date:To:References:In-Reply-To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=nWyxiR6q67EmqGbAlKnOyQDoc/aCBeLbg5bCnwUYbk2mM/2CyN3oSos9vRo43cxb5\n\tWIvGvL2JiLWvER1a/PWlsBoB8rIe0A7EqmCF5qgyoMxFNWSSySQ9NUxFDs6/xuVriB\n\ttp0FUQdX78q/OXCQ+XMEz/x3c4tbjjJQV+Ly551PP60/WUIux/hD+f3VftjM+HwQxZ\n\taqhHy9Oybi72Jd5eUapllbZRY/4z84zOkUM3vrLvzYrNncgIYa3vfropQW27//k/ZI\n\tQ/kRWTl8+q5kKJPPeuUYqfdwxBFK8UwjlYilGxWV/nlEaokYXuFmlRDJevNWVJEMoW\n\tJawoVEnOTdYBg==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1651834781;\n\tbh=nd6esZNhd2hGtFIOVGjJPgq3BC0eTt0J6CNl53h7/ZA=;\n\th=Date:Subject:To:Cc:References:From:In-Reply-To:From;\n\tb=RJDDvrmbsAHV+nzamjlkk+K17hH12J4oWII2fqG29MM8BUvYVW7QVQn+KUc1J4CPS\n\tr8D9xx1ipcnp1yLX5bI7Vj438WF7HnZdf3Hcg7RkKpIP4kSHfFjHm6XBtiYpednEdn\n\t6HLlv4Z1jTxl1nfzTnqKldhAqf4pfLyTHA9jx7m0="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"RJDDvrmb\"; dkim-atps=neutral","Message-ID":"<65d2c5d6-f70b-494d-015d-2d91b70fc60a@ideasonboard.com>","Date":"Fri, 6 May 2022 13:59:38 +0300","MIME-Version":"1.0","User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101\n\tThunderbird/91.8.0","Content-Language":"en-US","To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","References":"<20220505104104.70841-1-tomi.valkeinen@ideasonboard.com>\n\t<20220505104104.70841-7-tomi.valkeinen@ideasonboard.com>\n\t<YnQO+51KeS06rDc9@pendragon.ideasonboard.com>","In-Reply-To":"<YnQO+51KeS06rDc9@pendragon.ideasonboard.com>","Content-Type":"text/plain; charset=UTF-8; format=flowed","Content-Transfer-Encoding":"7bit","Subject":"Re: [libcamera-devel] [PATCH v7 06/13] py: add unittests.py","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","From":"Tomi Valkeinen via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":22889,"web_url":"https://patchwork.libcamera.org/comment/22889/","msgid":"<YnU+O6Xq4r22DM31@pendragon.ideasonboard.com>","date":"2022-05-06T15:26:51","subject":"Re: [libcamera-devel] [PATCH v7 06/13] py: add unittests.py","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Tomi,\n\nOn Fri, May 06, 2022 at 01:59:38PM +0300, Tomi Valkeinen wrote:\n> On 05/05/2022 20:52, Laurent Pinchart wrote:\n> > On Thu, May 05, 2022 at 01:40:57PM +0300, Tomi Valkeinen wrote:\n> >> Add a simple unittests.py as a base for python unittests.\n> >>\n> >> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> >> ---\n> >>   test/meson.build     |   1 +\n> >>   test/py/meson.build  |  17 ++\n> >>   test/py/unittests.py | 368 +++++++++++++++++++++++++++++++++++++++++++\n> >>   3 files changed, 386 insertions(+)\n> >>   create mode 100644 test/py/meson.build\n> >>   create mode 100755 test/py/unittests.py\n> >>\n> >> diff --git a/test/meson.build b/test/meson.build\n> >> index fd4c5ca0..623f3baa 100644\n> >> --- a/test/meson.build\n> >> +++ b/test/meson.build\n> >> @@ -18,6 +18,7 @@ subdir('log')\n> >>   subdir('media_device')\n> >>   subdir('pipeline')\n> >>   subdir('process')\n> >> +subdir('py')\n> >>   subdir('serialization')\n> >>   subdir('stream')\n> >>   subdir('v4l2_compat')\n> >> diff --git a/test/py/meson.build b/test/py/meson.build\n> >> new file mode 100644\n> >> index 00000000..f6b42bd0\n> >> --- /dev/null\n> >> +++ b/test/py/meson.build\n> >> @@ -0,0 +1,17 @@\n> >> +# SPDX-License-Identifier: CC0-1.0\n> >> +\n> >> +if not pycamera_enabled\n> >> +    subdir_done()\n> >> +endif\n> >> +\n> >> +pymod = import('python')\n> >> +py3 = pymod.find_installation('python3')\n> >> +\n> >> +pypathdir = meson.project_build_root() / 'src/py'\n> > \n> > pypathdir = meson.project_build_root() / 'src' / 'py'\n> > \n> >> +\n> >> +test('pyunittests',\n> >> +     py3,\n> >> +     args : files('unittests.py'),\n> >> +     env : ['PYTHONPATH=' + pypathdir],\n> >> +     suite : 'pybindings',\n> >> +     is_parallel : false)\n> >> diff --git a/test/py/unittests.py b/test/py/unittests.py\n> >> new file mode 100755\n> >> index 00000000..15d5b4a7\n> >> --- /dev/null\n> >> +++ b/test/py/unittests.py\n> >> @@ -0,0 +1,368 @@\n> >> +#!/usr/bin/env python3\n> >> +\n> >> +# SPDX-License-Identifier: GPL-2.0-or-later\n> >> +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> >> +\n> >> +from collections import defaultdict\n> >> +import errno\n> >> +import gc\n> >> +import libcamera as libcam\n> > \n> > I'm tempted to claim the \"cam\" name in all our Python code, or possibly\n> > \"camera\". What do you think ?\n> \n> I think 'cam' and 'camera' are names for variables that contain a camera.\n\nThat's a very good point, even if \"cam\" would also be a good name for\nthe module. Any idea from anyone on what would be a good short module\nname alias ? We could also use it as a libcamera namespace alias in C++\ncode examples for consistency.\n\n> >> +import os\n> >> +import selectors\n> >> +import time\n> >> +import unittest\n> >> +import weakref\n> >> +\n> >> +\n> >> +class MyTestCase(unittest.TestCase):\n> > \n> > s/MyTestCase/BaseTestCase/\n> > \n> > It's not yours only anymore :-)\n> \n> My precious...\n> \n> >> +    def assertZero(self, a, msg=None):\n> >> +        self.assertEqual(a, 0, msg)\n> >> +\n> >> +\n> >> +class SimpleTestMethods(MyTestCase):\n> >> +    def test_find_ref(self):\n> >> +        cm = libcam.CameraManager.singleton()\n> >> +        wr_cm = weakref.ref(cm)\n> >> +\n> >> +        cam = cm.find(\"platform/vimc\")\n> > \n> > Standardizing on single-quotes here too would be nice.\n> \n> I can't stand them, but... Fine, it's probably best to follow the widely \n> used convention.\n\nI think there's a PEP that recommends single quotes. It took me a while\nto get used to it.\n\n> >> +        self.assertIsNotNone(cam)\n> >> +        wr_cam = weakref.ref(cam)\n> >> +\n> >> +        cm = None\n> >> +        gc.collect()\n> >> +        self.assertIsNotNone(wr_cm())\n> >> +\n> >> +        cam = None\n> >> +        gc.collect()\n> >> +        self.assertIsNone(wr_cm())\n> >> +        self.assertIsNone(wr_cam())\n> >> +\n> >> +    def test_get_ref(self):\n> >> +        cm = libcam.CameraManager.singleton()\n> >> +        wr_cm = weakref.ref(cm)\n> >> +\n> >> +        cam = cm.get(\"platform/vimc.0 Sensor B\")\n> >> +        self.assertTrue(cam is not None)\n> >> +        wr_cam = weakref.ref(cam)\n> >> +\n> >> +        cm = None\n> >> +        gc.collect()\n> >> +        self.assertIsNotNone(wr_cm())\n> >> +\n> >> +        cam = None\n> >> +        gc.collect()\n> >> +        self.assertIsNone(wr_cm())\n> >> +        self.assertIsNone(wr_cam())\n> >> +\n> >> +    def test_acquire_release(self):\n> >> +        cm = libcam.CameraManager.singleton()\n> >> +        cam = cm.get(\"platform/vimc.0 Sensor B\")\n> >> +        self.assertTrue(cam is not None)\n> >> +\n> >> +        ret = cam.acquire()\n> >> +        self.assertZero(ret)\n> >> +\n> >> +        ret = cam.release()\n> >> +        self.assertZero(ret)\n> >> +\n> >> +    def test_double_acquire(self):\n> >> +        cm = libcam.CameraManager.singleton()\n> >> +        cam = cm.get(\"platform/vimc.0 Sensor B\")\n> >> +        self.assertTrue(cam is not None)\n> >> +\n> >> +        ret = cam.acquire()\n> >> +        self.assertZero(ret)\n> >> +\n> >> +        libcam.logSetLevel(\"Camera\", \"FATAL\")\n> >> +        ret = cam.acquire()\n> >> +        self.assertEqual(ret, -errno.EBUSY)\n> >> +        libcam.logSetLevel(\"Camera\", \"ERROR\")\n> >> +\n> >> +        ret = cam.release()\n> >> +        self.assertZero(ret)\n> >> +\n> >> +        ret = cam.release()\n> >> +        # I expected EBUSY, but looks like double release works fine\n> >> +        self.assertZero(ret)\n> >> +\n> >> +\n> >> +class CameraTesterBase(MyTestCase):\n> >> +    def setUp(self):\n> >> +        self.cm = libcam.CameraManager.singleton()\n> >> +        self.cam = self.cm.find(\"platform/vimc\")\n> >> +        if self.cam is None:\n> >> +            self.cm = None\n> >> +            raise Exception(\"No vimc found\")\n> > \n> > Is it possible to skip the test in that case instead of failing ? This\n> > could be done on top, a todo comment somewhere in the file would be good\n> > then.\n> \n> Yes, I can do that. Although some tests test finding the vimc camera, so \n> those will still fail.\n> \n> >> +\n> >> +        ret = self.cam.acquire()\n> >> +        if ret != 0:\n> >> +            self.cam = None\n> >> +            self.cm = None\n> >> +            raise Exception(\"Failed to acquire camera\")\n> >> +\n> >> +    def tearDown(self):\n> >> +        # If a test fails, the camera may be in running state. So always stop.\n> >> +        self.cam.stop()\n> >> +\n> >> +        ret = self.cam.release()\n> >> +        if ret != 0:\n> >> +            raise Exception(\"Failed to release camera\")\n> >> +\n> >> +        self.cam = None\n> >> +        self.cm = None\n> >> +\n> >> +\n> >> +class AllocatorTestMethods(CameraTesterBase):\n> >> +    def test_allocator(self):\n> >> +        cam = self.cam\n> >> +\n> >> +        camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture])\n> >> +        self.assertTrue(camconfig.size == 1)\n> >> +        wr_camconfig = weakref.ref(camconfig)\n> >> +\n> >> +        streamconfig = camconfig.at(0)\n> >> +        wr_streamconfig = weakref.ref(streamconfig)\n> >> +\n> >> +        ret = cam.configure(camconfig)\n> >> +        self.assertZero(ret)\n> >> +\n> >> +        stream = streamconfig.stream\n> >> +        wr_stream = weakref.ref(stream)\n> >> +\n> >> +        # stream should keep streamconfig and camconfig alive\n> >> +        streamconfig = None\n> >> +        camconfig = None\n> >> +        gc.collect()\n> >> +        self.assertIsNotNone(wr_camconfig())\n> >> +        self.assertIsNotNone(wr_streamconfig())\n> >> +\n> >> +        allocator = libcam.FrameBufferAllocator(cam)\n> >> +        ret = allocator.allocate(stream)\n> >> +        self.assertTrue(ret > 0)\n> >> +        wr_allocator = weakref.ref(allocator)\n> >> +\n> >> +        buffers = allocator.buffers(stream)\n> >> +        buffers = None\n> >> +\n> >> +        buffer = allocator.buffers(stream)[0]\n> >> +        self.assertIsNotNone(buffer)\n> >> +        wr_buffer = weakref.ref(buffer)\n> >> +\n> >> +        allocator = None\n> >> +        gc.collect()\n> >> +        self.assertIsNotNone(wr_buffer())\n> >> +        self.assertIsNotNone(wr_allocator())\n> >> +        self.assertIsNotNone(wr_stream())\n> >> +\n> >> +        buffer = None\n> >> +        gc.collect()\n> >> +        self.assertIsNone(wr_buffer())\n> >> +        self.assertIsNone(wr_allocator())\n> >> +        self.assertIsNotNone(wr_stream())\n> >> +\n> >> +        stream = None\n> >> +        gc.collect()\n> >> +        self.assertIsNone(wr_stream())\n> >> +        self.assertIsNone(wr_camconfig())\n> >> +        self.assertIsNone(wr_streamconfig())\n> >> +\n> >> +\n> >> +class SimpleCaptureMethods(CameraTesterBase):\n> >> +    def test_sleep(self):\n> >> +        cm = self.cm\n> >> +        cam = self.cam\n> >> +\n> >> +        camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture])\n> >> +        self.assertTrue(camconfig.size == 1)\n> >> +\n> >> +        streamconfig = camconfig.at(0)\n> >> +        fmts = streamconfig.formats\n> >> +\n> >> +        ret = cam.configure(camconfig)\n> >> +        self.assertZero(ret)\n> >> +\n> >> +        stream = streamconfig.stream\n> >> +\n> >> +        allocator = libcam.FrameBufferAllocator(cam)\n> >> +        ret = allocator.allocate(stream)\n> >> +        self.assertTrue(ret > 0)\n> >> +\n> >> +        num_bufs = len(allocator.buffers(stream))\n> >> +\n> >> +        reqs = []\n> >> +        for i in range(num_bufs):\n> >> +            req = cam.createRequest(i)\n> >> +            self.assertIsNotNone(req)\n> >> +\n> >> +            buffer = allocator.buffers(stream)[i]\n> >> +            ret = req.addBuffer(stream, buffer)\n> >> +            self.assertZero(ret)\n> >> +\n> >> +            reqs.append(req)\n> >> +\n> >> +        buffer = None\n> >> +\n> >> +        ret = cam.start()\n> >> +        self.assertZero(ret)\n> >> +\n> >> +        for req in reqs:\n> >> +            ret = cam.queueRequest(req)\n> >> +            self.assertZero(ret)\n> >> +\n> >> +        reqs = None\n> >> +        gc.collect()\n> >> +\n> >> +        time.sleep(0.5)\n> >> +\n> >> +        reqs = cm.getReadyRequests()\n> >> +\n> >> +        self.assertTrue(len(reqs) == num_bufs)\n> >> +\n> >> +        for i, req in enumerate(reqs):\n> >> +            self.assertTrue(i == req.cookie)\n> >> +\n> >> +        reqs = None\n> >> +        gc.collect()\n> >> +\n> >> +        ret = cam.stop()\n> >> +        self.assertZero(ret)\n> >> +\n> >> +    def test_select(self):\n> >> +        cm = self.cm\n> >> +        cam = self.cam\n> >> +\n> >> +        camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture])\n> >> +        self.assertTrue(camconfig.size == 1)\n> >> +\n> >> +        streamconfig = camconfig.at(0)\n> >> +        fmts = streamconfig.formats\n> >> +\n> >> +        ret = cam.configure(camconfig)\n> >> +        self.assertZero(ret)\n> >> +\n> >> +        stream = streamconfig.stream\n> >> +\n> >> +        allocator = libcam.FrameBufferAllocator(cam)\n> >> +        ret = allocator.allocate(stream)\n> >> +        self.assertTrue(ret > 0)\n> >> +\n> >> +        num_bufs = len(allocator.buffers(stream))\n> >> +\n> >> +        reqs = []\n> >> +        for i in range(num_bufs):\n> >> +            req = cam.createRequest(i)\n> >> +            self.assertIsNotNone(req)\n> >> +\n> >> +            buffer = allocator.buffers(stream)[i]\n> >> +            ret = req.addBuffer(stream, buffer)\n> >> +            self.assertZero(ret)\n> >> +\n> >> +            reqs.append(req)\n> >> +\n> >> +        buffer = None\n> >> +\n> >> +        ret = cam.start()\n> >> +        self.assertZero(ret)\n> >> +\n> >> +        for req in reqs:\n> >> +            ret = cam.queueRequest(req)\n> >> +            self.assertZero(ret)\n> >> +\n> >> +        reqs = None\n> >> +        gc.collect()\n> >> +\n> >> +        sel = selectors.DefaultSelector()\n> >> +        sel.register(cm.efd, selectors.EVENT_READ, 123)\n> > \n> > Is the data argument needed ?\n> \n> No, it's not.\n> \n> >> +\n> >> +        reqs = []\n> >> +\n> >> +        running = True\n> >> +        while running:\n> >> +            events = sel.select()\n> >> +            for key, mask in events:\n> >> +                os.read(key.fileobj, 8)\n> >> +\n> >> +                ready_reqs = cm.getReadyRequests()\n> >> +\n> >> +                self.assertTrue(len(ready_reqs) > 0)\n> >> +\n> >> +                reqs += ready_reqs\n> >> +\n> >> +                if len(reqs) == num_bufs:\n> >> +                    running = False\n> >> +\n> >> +        self.assertTrue(len(reqs) == num_bufs)\n> >> +\n> >> +        for i, req in enumerate(reqs):\n> >> +            self.assertTrue(i == req.cookie)\n> >> +\n> >> +        reqs = None\n> >> +        gc.collect()\n> >> +\n> >> +        ret = cam.stop()\n> >> +        self.assertZero(ret)\n> >> +\n> >> +\n> >> +# Recursively expand slist's objects into olist, using seen to track already\n> >> +# processed objects.\n> >> +def _getr(slist, olist, seen):\n> >> +    for e in slist:\n> >> +        if id(e) in seen:\n> >> +            continue\n> >> +        seen.add(id(e))\n> >> +        olist.append(e)\n> >> +        tl = gc.get_referents(e)\n> >> +        if tl:\n> >> +            _getr(tl, olist, seen)\n> >> +\n> >> +\n> >> +def get_all_objects(ignored=[]):\n> >> +    gcl = gc.get_objects()\n> >> +    olist = []\n> >> +    seen = set()\n> >> +\n> >> +    seen.add(id(gcl))\n> >> +    seen.add(id(olist))\n> >> +    seen.add(id(seen))\n> >> +    seen.update(set([id(o) for o in ignored]))\n> >> +\n> >> +    _getr(gcl, olist, seen)\n> >> +\n> >> +    return olist\n> >> +\n> >> +\n> >> +def create_type_count_map(olist):\n> >> +    map = defaultdict(int)\n> >> +    for o in olist:\n> >> +        map[type(o)] += 1\n> >> +    return map\n> >> +\n> >> +\n> >> +def diff_type_count_maps(before, after):\n> >> +    return [(k, after[k] - before[k]) for k in after if after[k] != before[k]]\n> >> +\n> >> +\n> >> +if __name__ == '__main__':\n> >> +    # doesn't work very well, as things always leak a bit\n> > \n> > Lovely :-) Is it something we can fix ?\n> \n> I have no idea if this method is a valid way to observe object leaks, \n> so... no clue. I think it can still be used, but manually, comparing \n> object lists when doing some changes to the tests.\n\nWe can figure it out later.","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id E7B33C3256\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  6 May 2022 15:26:57 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 343A7604A3;\n\tFri,  6 May 2022 17:26:57 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id C4C45604A3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  6 May 2022 17:26:55 +0200 (CEST)","from pendragon.ideasonboard.com (62-78-145-57.bb.dnainternet.fi\n\t[62.78.145.57])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 4F817492;\n\tFri,  6 May 2022 17:26:55 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1651850817;\n\tbh=QNhOix6eT562LAQdxOY0Fn+m4rTsdi2Xu+mcgC6S2JM=;\n\th=Date:To:References:In-Reply-To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=xvZBTrSsTWtLWDWG0vSX+Y+QpJQzGEkRqFLqMv/DzrjmDm54PMPOW/zf7ov+9hYox\n\tRZnS3wZw59mj3BNe4syaM+5y+BKFVq2WoaXaMkUBthXoCQM0TutFYbC3nB2bliT6Ek\n\tVjbop2QurvduvIoGPLwJ19y/YE3U4i2BqAbb9E7YtuiDzqb6cpjmTKX/Cjh9BKwXsR\n\tm05neY2OLsUYYcMQoru4FLwqNGRIRNBO7STM1gRR+QartKc/HnSFsOejNMfA7Jiixs\n\tRsawMHTYJqCrjhQ4dLXeCUPpc65g/E75iyho4r2Vij1GoBrMji3Sm+E2cAvm2E26Lz\n\tLFQP+JsHKsOjA==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1651850815;\n\tbh=QNhOix6eT562LAQdxOY0Fn+m4rTsdi2Xu+mcgC6S2JM=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=MtGgB6zfJgcDqs+cAJrbQnGAMBVWo5sNqkJ0uAhIpLwJ6oLWLKWu5hze3QOqtD6hn\n\tDO+NVEcdx5UjXPmygWBF1Q28vmRNzzfGgmmlkZip2I8L1f6DDS5VNJgyPjiphsSxar\n\tdxovL8yJoQgK0kjpcLtMlwyMcMly4smjSrTKuw2k="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"MtGgB6zf\"; dkim-atps=neutral","Date":"Fri, 6 May 2022 18:26:51 +0300","To":"Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Message-ID":"<YnU+O6Xq4r22DM31@pendragon.ideasonboard.com>","References":"<20220505104104.70841-1-tomi.valkeinen@ideasonboard.com>\n\t<20220505104104.70841-7-tomi.valkeinen@ideasonboard.com>\n\t<YnQO+51KeS06rDc9@pendragon.ideasonboard.com>\n\t<65d2c5d6-f70b-494d-015d-2d91b70fc60a@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<65d2c5d6-f70b-494d-015d-2d91b70fc60a@ideasonboard.com>","Subject":"Re: [libcamera-devel] [PATCH v7 06/13] py: add unittests.py","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","From":"Laurent Pinchart via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]