[PATCH 6/7] tests/cooker: add TinfoilTests for run_prepared_task

AdrianF <[email protected]>
Newsgroups org.openembedded.lists.bitbake-devel
Message-ID <[email protected]>
From: Adrian Freihofer <[email protected]>

Add TinfoilTests covering 'tinfoil: add a prepared task runner'. Each
test spawns a subprocess to isolate tinfoil's server lifecycle.

TestEquivHash is needed because the noop siggen's invalidate_task()
removes the base stamp path instead of the task-specific one, making
force=True a no-op otherwise.

Lives in cooker.py rather than runqueue.py since it tests Tinfoil's
Python API, not CLI-level runqueue behaviour.

AI-Generated: Uses GitHub Copilot

Signed-off-by: Adrian Freihofer <[email protected]>
---
 lib/bb/tests/cooker.py | 159 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 159 insertions(+)

diff --git a/lib/bb/tests/cooker.py b/lib/bb/tests/cooker.py
index 76ec65540..a08843e98 100644
--- a/lib/bb/tests/cooker.py
+++ b/lib/bb/tests/cooker.py
@@ -9,12 +9,171 @@
 import unittest
 import os
 import subprocess
+import sys
 import tempfile
+import time
 import bb, bb.cooker
 import re
 import logging
 
 # Cooker tests
+
+
+class TinfoilTests(unittest.TestCase):
+    """Tests for the Tinfoil API that require a running bitbake server."""
+
+    # Library directory containing bb.tinfoil
+    _bblib = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))
+    # runqueue-tests BBPATH (provides the simple a1/b1/... test recipes)
+    _runqueuetests = os.path.realpath(os.path.join(os.path.dirname(__file__), 'runqueue-tests'))
+
+    failing_recipe = """\
+python do_install() {
+    bb.fatal("deliberate failure")
+}
+addtask install
+"""
+
+    def _make_env(self, builddir, extra=None):
+        env = os.environ.copy()
+        env['PYTHONPATH'] = self._bblib + (':' + env['PYTHONPATH'] if 'PYTHONPATH' in env else '')
+        env['BBPATH'] = self._runqueuetests
+        env['BB_ENV_PASSTHROUGH_ADDITIONS'] = 'SSTATEVALID SLOWTASKS TOPDIR BB_HASHSERVE BB_SIGNATURE_HANDLER EXTRA_BBFILES'
+        env['SSTATEVALID'] = ''
+        env['SLOWTASKS'] = ''
+        env['TOPDIR'] = builddir
+        # TestEquivHash creates taint files so that force=True actually
+        # invalidates the task hash; the default noop siggen cannot do this.
+        env['BB_HASHSERVE'] = 'auto'
+        env['BB_SIGNATURE_HANDLER'] = 'TestEquivHash'
+        if extra:
+            env.update(extra)
+        return env
+
+    def _run_script(self, builddir, script, extra=None):
+        """Run script in a subprocess to isolate tinfoil's server lifecycle."""
+        proc = subprocess.run(
+            [sys.executable, '-c', script],
+            env=self._make_env(builddir, extra),
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+            cwd=builddir,
+        )
+        if proc.returncode:
+            self.fail('tinfoil script failed: %s' % proc.stdout)
+        return proc.stdout
+
+    def _read_tasklog(self, builddir, cleanup=True):
+        tasklog = os.path.join(builddir, 'task.log')
+        tasks = []
+        if os.path.exists(tasklog):
+            with open(tasklog) as f:
+                tasks = [line.rstrip() for line in f]
+            if cleanup:
+                os.remove(tasklog)
+        return tasks
+
+    def _shutdown(self, builddir):
+        """Wait for the bitbake server and hashserv to release builddir.
+
+        Must run before the caller's TemporaryDirectory is removed, so it
+        cannot be a tearDown().
+        """
+        deadline = time.monotonic() + 30
+        while time.monotonic() < deadline:
+            if not any(os.path.exists(os.path.join(builddir, p))
+                       for p in ('hashserve.sock', 'bitbake.lock')):
+                return
+            time.sleep(0.5)
+
+    def test_run_prepared_task(self):
+        """tinfoil.run_prepared_task() reruns one task without resolving deps.
+
+        Uses do_install since that's the real devtool ide-sdk scenario: it
+        needs pseudo and so must run via bitbake, unlike do_compile which the
+        IDE invokes directly (e.g. via cmake/meson).
+
+        Builds a1 completely so all stamps/hashes are valid, then calls
+        run_prepared_task('a1', 'install') through the Python API and verifies
+        that only do_install re-runs while its intra-recipe predecessors
+        (fetch, unpack, patch, prepare_recipe_sysroot, configure, compile) are
+        skipped.
+        """
+        # The script runs inside a subprocess so that tinfoil's server
+        # lifecycle and environment modifications are isolated.
+        script = """
+import os, sys
+import bb.tinfoil
+
+builddir = os.environ['TOPDIR']
+tasklog  = os.path.join(builddir, 'task.log')
+
+with bb.tinfoil.Tinfoil() as tinfoil:
+    tinfoil.prepare(quiet=2)
+    # Full build so all stamps and hashes are valid.
+    tinfoil.build_targets(['a1'])
+    # Clear the log so only the run_prepared_task() entries are counted.
+    if os.path.exists(tasklog):
+        os.remove(tasklog)
+    # run_prepared_task() sets force=True (taint) and calls build_file_sync
+    # with the recipe file resolved via get_recipe_file(), bypassing the
+    # normal runqueue dependency resolver.
+    tinfoil.run_prepared_task('a1', 'install')
+"""
+        with tempfile.TemporaryDirectory(prefix='tinfoiltest') as builddir:
+            try:
+                self._run_script(builddir, script)
+
+                tasks = self._read_tasklog(builddir)
+                self.assertEqual(tasks, ['a1:install'],
+                                 'run_prepared_task should rerun only install, got: %s' % tasks)
+            finally:
+                self._shutdown(builddir)
+
+    def test_run_prepared_task_unbuilt(self):
+        """run_prepared_task() runs the task and nothing else.
+
+        The recipe was never built, so if any dependency task were still in
+        the runqueue it would have to run here.
+        """
+        script = """
+import bb.tinfoil
+
+with bb.tinfoil.Tinfoil() as tinfoil:
+    tinfoil.prepare(quiet=2)
+    assert tinfoil.run_prepared_task('a1', 'install') is True
+"""
+        with tempfile.TemporaryDirectory(prefix='tinfoiltest') as builddir:
+            try:
+                self._run_script(builddir, script)
+
+                tasks = self._read_tasklog(builddir)
+                self.assertEqual(tasks, ['a1:install'],
+                                 'run_prepared_task should run no dependency task, got: %s' % tasks)
+            finally:
+                self._shutdown(builddir)
+
+    def test_run_prepared_task_failure(self):
+        """A failing task makes run_prepared_task() return False, not raise."""
+        script = """
+import bb.tinfoil
+
+with bb.tinfoil.Tinfoil() as tinfoil:
+    tinfoil.prepare(quiet=2)
+    assert tinfoil.run_prepared_task('failer', 'install') is False
+"""
+        with tempfile.TemporaryDirectory(prefix='tinfoilrecipes') as recipes, \
+             tempfile.TemporaryDirectory(prefix='tinfoiltest') as builddir:
+            with open(os.path.join(recipes, 'failer.bb'), 'w') as f:
+                f.write(self.failing_recipe)
+            try:
+                self._run_script(builddir, script,
+                                 {'EXTRA_BBFILES': '%s/*.bb' % recipes})
+            finally:
+                self._shutdown(builddir)
+
+
 class CookerTest(unittest.TestCase):
     def setUp(self):
         # At least one variable needs to be set
-- 
2.55.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.