diff mbox series

[35/41] oeqa/core/decorator: Record decorators on the test function

Message ID 20261115084845.701136-35-richard.purdie@linuxfoundation.org
State New
Headers show
Series [01/41] oeqa/loader: Drop code for pre python 3.5 | expand

Commit Message

Richard Purdie Nov. 15, 2026, 8:48 a.m. UTC
OETestDecorator wrapped each test method in a function which only called
it, with a dummy assignment to self so the decorator would be captured
in the wrapper's closure. OETestLoader then walked the closures to find
the decorators.

Instead, record the decorators in an attribute on the test function and
return it unchanged. The order they are set up in is the same as before,
innermost first, and a test now checks this.

AI-Generated: Uses Claude Opus 5.5
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
---
 meta/lib/oeqa/core/decorator/__init__.py    | 10 ++++----
 meta/lib/oeqa/core/loader.py                | 26 +++++++--------------
 meta/lib/oeqa/core/tests/test_decorators.py |  8 +++++++
 3 files changed, 20 insertions(+), 24 deletions(-)
diff mbox series

Patch

diff --git a/meta/lib/oeqa/core/decorator/__init__.py b/meta/lib/oeqa/core/decorator/__init__.py
index 93efd30e1db..e3e1326873c 100644
--- a/meta/lib/oeqa/core/decorator/__init__.py
+++ b/meta/lib/oeqa/core/decorator/__init__.py
@@ -4,7 +4,6 @@ 
 # SPDX-License-Identifier: MIT
 #
 
-from functools import wraps
 from abc import ABCMeta
 
 decoratorClasses = set()
@@ -29,11 +28,10 @@  class OETestDecorator(object, metaclass=ABCMeta):
             setattr(self, attr, value)
 
     def __call__(self, func):
-        @wraps(func)
-        def wrapped_f(*args, **kwargs):
-            self.attrs = self.attrs # XXX: Enables OETestLoader discover
-            return func(*args, **kwargs)
-        return wrapped_f
+        # Record the decorator on the test function for OETestLoader to find.
+        # They are set up in the order they are applied, innermost first.
+        func._oeqa_decorators = getattr(func, '_oeqa_decorators', []) + [self]
+        return func
 
     # OETestLoader call it when is loading test cases.
     # XXX: Most methods would change the registry for later
diff --git a/meta/lib/oeqa/core/loader.py b/meta/lib/oeqa/core/loader.py
index 28b4fd20166..94ade54506d 100644
--- a/meta/lib/oeqa/core/loader.py
+++ b/meta/lib/oeqa/core/loader.py
@@ -14,8 +14,7 @@  from oeqa.core.utils.test import getSuiteModules, getCaseID
 
 from oeqa.core.exception import OEQATestNotFound
 from oeqa.core.case import OETestCase
-from oeqa.core.decorator import decoratorClasses, OETestDecorator, \
-        OETestDiscover
+from oeqa.core.decorator import decoratorClasses, OETestDiscover
 
 # When loading tests, the unittest framework stores any exceptions and
 # displays them only when the run method is called.
@@ -103,23 +102,14 @@  class OETestLoader(unittest.TestLoader):
         self.tc._registry['cases'][case_id] = case
 
     def _handleTestCaseDecorators(self, case):
-        def _handle(obj):
-            if isinstance(obj, OETestDecorator):
-                if not obj.__class__ in decoratorClasses:
-                    raise Exception("Decorator %s isn't registered" \
-                            " in decoratorClasses." % obj.__class__.__name__)
-                # The decorator instance is shared by every class that has
-                # the method so bind a copy for this test case
-                copy.copy(obj).bind(self.tc._registry, case)
-
-        def _walk_closure(obj):
-            if hasattr(obj, '__closure__') and obj.__closure__:
-                for f in obj.__closure__:
-                    obj = f.cell_contents
-                    _handle(obj)
-                    _walk_closure(obj)
         method = getattr(case, case._testMethodName, None)
-        _walk_closure(method)
+        for obj in getattr(method, '_oeqa_decorators', []):
+            if not obj.__class__ in decoratorClasses:
+                raise Exception("Decorator %s isn't registered" \
+                        " in decoratorClasses." % obj.__class__.__name__)
+            # The decorator instance is shared by every class that has
+            # the method so bind a copy for this test case
+            copy.copy(obj).bind(self.tc._registry, case)
 
     def _filterTest(self, case):
         """
diff --git a/meta/lib/oeqa/core/tests/test_decorators.py b/meta/lib/oeqa/core/tests/test_decorators.py
index 74ac4632372..90ab1d3ff67 100755
--- a/meta/lib/oeqa/core/tests/test_decorators.py
+++ b/meta/lib/oeqa/core/tests/test_decorators.py
@@ -126,6 +126,14 @@  class TestDependsDecorator(TestBase):
         self.assertEqual(result.skipped, [])
         self.assertEqual(len(result.successes), len(tests))
 
+class TestDecoratorOrder(TestBase):
+    def test_decorator_order(self):
+        # Decorators are set up in the order they are applied, innermost first
+        tc = self._testLoader(modules=['timeout'])
+        case = tc._registry['cases']['timeout.TimeoutTest.testTimeoutDepends']
+        self.assertEqual([d.__class__.__name__ for d in case.decorators],
+                         ['OETimeout', 'OETestDepends'])
+
 class TestInheritedDecorators(TestBase):
     modules = ['inherit']