diff mbox series

[29/41] oeqa/core/loader: Bind a copy of each decorator per test case

Message ID 20261115084845.701136-29-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
A decorator instance belongs to the decorated function, so when a test
method is inherited by more than one test class the same instance was
bound to each test case in turn and ended up referring to whichever case
was bound last. Its setUpDecorator() then ran against the wrong case.
Bind a copy for each test case instead and add a test.

Also fix the unregistered decorator error, which used obj.__name__ on a
decorator instance and so raised AttributeError instead.

AI-Generated: Uses Claude Opus 5.5
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
---
 meta/lib/oeqa/core/loader.py                |  7 +++++--
 meta/lib/oeqa/core/tests/cases/inherit.py   | 20 ++++++++++++++++++++
 meta/lib/oeqa/core/tests/test_decorators.py | 14 ++++++++++++++
 3 files changed, 39 insertions(+), 2 deletions(-)
 create mode 100644 meta/lib/oeqa/core/tests/cases/inherit.py
diff mbox series

Patch

diff --git a/meta/lib/oeqa/core/loader.py b/meta/lib/oeqa/core/loader.py
index 90b9366754d..28b4fd20166 100644
--- a/meta/lib/oeqa/core/loader.py
+++ b/meta/lib/oeqa/core/loader.py
@@ -4,6 +4,7 @@ 
 # SPDX-License-Identifier: MIT
 #
 
+import copy
 import re
 import sys
 import unittest
@@ -106,8 +107,10 @@  class OETestLoader(unittest.TestLoader):
             if isinstance(obj, OETestDecorator):
                 if not obj.__class__ in decoratorClasses:
                     raise Exception("Decorator %s isn't registered" \
-                            " in decoratorClasses." % obj.__name__)
-                obj.bind(self.tc._registry, case)
+                            " 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__:
diff --git a/meta/lib/oeqa/core/tests/cases/inherit.py b/meta/lib/oeqa/core/tests/cases/inherit.py
new file mode 100644
index 00000000000..8600c8005b5
--- /dev/null
+++ b/meta/lib/oeqa/core/tests/cases/inherit.py
@@ -0,0 +1,20 @@ 
+#
+# Copyright OpenEmbedded Contributors
+#
+# SPDX-License-Identifier: MIT
+#
+
+from oeqa.core.case import OETestCase
+from oeqa.core.decorator.depends import OETestDepends
+
+class InheritBaseTest(OETestCase):
+
+    def testInheritFirst(self):
+        self.assertTrue(True, msg='How is this possible?')
+
+    @OETestDepends(['testInheritFirst'])
+    def testInheritSecond(self):
+        self.assertTrue(True, msg='How is this possible?')
+
+class InheritSubTest(InheritBaseTest):
+    pass
diff --git a/meta/lib/oeqa/core/tests/test_decorators.py b/meta/lib/oeqa/core/tests/test_decorators.py
index d3305b1e534..74ac4632372 100755
--- a/meta/lib/oeqa/core/tests/test_decorators.py
+++ b/meta/lib/oeqa/core/tests/test_decorators.py
@@ -126,6 +126,20 @@  class TestDependsDecorator(TestBase):
         self.assertEqual(result.skipped, [])
         self.assertEqual(len(result.successes), len(tests))
 
+class TestInheritedDecorators(TestBase):
+    modules = ['inherit']
+
+    def test_inherited_decorators(self):
+        # Each test case needs its own decorator instances, even when the
+        # test method is inherited by another test class
+        tc = self._testLoader(modules=self.modules)
+        cases = tc._registry['cases']
+        self.assertEqual(len(cases), 4)
+        for case in cases.values():
+            for d in case.decorators:
+                self.assertIs(d.case, case)
+        self.assertTrue(tc.runTests().wasSuccessful())
+
 class TestTimeoutDecorator(TestBase):
     modules = ['timeout']