new file mode 100644
@@ -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()
@@ -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)
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