@@ -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