diff mbox series

[04/41] oeqa/core/utils/concurrencytest: Fix losing results after class setup failures

Message ID 20261115084845.701136-4-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
When setUpClass() or setUpModule() fails or skips in a test process,
the result is reported outside of any test. The subunit parser patches
which handle this reported the outcome and the stopTest() which follows
it without a startTest(). testtools tracks per test state between those
calls, so this broke its forwarding result and the thread reading that
process' results crashed on the next test, losing the results of all the
remaining tests in the process. These were then reported as UNKNOWN.

Start the placeholder test before reporting its outcome, as the parser
does for normal tests, and add a test comparing results from a
concurrent run with a normal run. The test is skipped if testtools,
subunit or bitbake aren't available.

AI-Generated: Uses Claude Opus 5.5
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
---
 meta/lib/oeqa/core/tests/test_concurrency.py | 42 ++++++++++++++++++++
 meta/lib/oeqa/core/utils/concurrencytest.py  |  6 +++
 2 files changed, 48 insertions(+)
 create mode 100644 meta/lib/oeqa/core/tests/test_concurrency.py
diff mbox series

Patch

diff --git a/meta/lib/oeqa/core/tests/test_concurrency.py b/meta/lib/oeqa/core/tests/test_concurrency.py
new file mode 100644
index 00000000000..16e6619ad7f
--- /dev/null
+++ b/meta/lib/oeqa/core/tests/test_concurrency.py
@@ -0,0 +1,42 @@ 
+#!/usr/bin/env python3
+#
+# Copyright OpenEmbedded Contributors
+#
+# SPDX-License-Identifier: MIT
+#
+
+import os
+import unittest
+from unittest import mock
+
+from common import setup_sys_path, TestBase
+setup_sys_path()
+
+try:
+    from oeqa.core.utils.concurrencytest import ConcurrentTestSuite
+except ImportError:
+    ConcurrentTestSuite = None
+
+@unittest.skipIf(ConcurrentTestSuite is None, "concurrencytest needs testtools, subunit and bitbake")
+class TestConcurrency(TestBase):
+    def _statuses(self, modules, processes=None):
+        tc = self._testLoader(modules=modules)
+        if processes:
+            setup = lambda suffix, selftestdir, suite: (None, None)
+            tc.prepareSuite = lambda suites, processes: ConcurrentTestSuite(suites, processes, setup, None, {'BBLAYERS': ''})
+        # fork_for_tests() only looks up the selftest layer with BUILDDIR set
+        with mock.patch.dict(os.environ, {'BUILDDIR': '/nonexistent'}):
+            result = tc.runTests(processes=processes)
+        return {case_id: result._getTestResultDetails(case)[0]
+                for case_id, case in tc._registry['cases'].items()}
+
+    def test_concurrent_status(self):
+        # Class and module setup failures must not stop the results of the
+        # other tests run by the same process being reported
+        modules = ['results', 'resultsmodule', 'subtest']
+        sequential = self._statuses(modules)
+        self.assertNotIn('UNKNOWN', sequential.values())
+        self.assertEqual(self._statuses(modules, processes=1), sequential)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/meta/lib/oeqa/core/utils/concurrencytest.py b/meta/lib/oeqa/core/utils/concurrencytest.py
index 5a760c620b2..09d621446ee 100644
--- a/meta/lib/oeqa/core/utils/concurrencytest.py
+++ b/meta/lib/oeqa/core/utils/concurrencytest.py
@@ -156,6 +156,9 @@  def outSideTestaddError(self, offset, line):
     test_name = line[offset:-1].decode('utf8')
     self.parser._current_test = subunit.RemotedTestCase(test_name)
     self.parser.current_test_description = test_name
+    # The outcome is followed by stopTest(), which testtools needs to match
+    # a startTest() or it loses track and breaks on the following tests
+    self.parser.client.startTest(self.parser._current_test)
     self.parser._state = self.parser._reading_error_details
     self.parser._reading_error_details.set_simple()
     self.parser.subunitLineReceived(line)
@@ -171,6 +174,9 @@  def outSideTestaddSkip(self, offset, line):
     test_name = line[offset:-1].decode('utf8')
     self.parser._current_test = subunit.RemotedTestCase(test_name)
     self.parser.current_test_description = test_name
+    # The outcome is followed by stopTest(), which testtools needs to match
+    # a startTest() or it loses track and breaks on the following tests
+    self.parser.client.startTest(self.parser._current_test)
     self.parser._state = self.parser._reading_skip_details
     self.parser._reading_skip_details.set_simple()
     self.parser.subunitLineReceived(line)