diff mbox series

[layerindex-web,1/3] layerindex: record Update Layer runs in a single update record

Message ID 20261114003041.15197-2-tim.orling@konsulko.com
State Under Review
Delegated to: Tim Orling
Headers show
Series Fix the Update Layer button | expand

Commit Message

Tim Orling Nov. 14, 2026, 12:30 a.m. UTC
The Update Layer button created two update records: one from the view,
with the task id and log, and another from update.py, with the layer
updates. Add a --update option to update.py, as other update tools
have, to record its work in an existing record, and use it from the
view. Save only the fields each side sets, so that the task and
update.py don't overwrite each other's changes.

[YOCTO #15395]

AI-Generated: Claude Opus 5.5
Signed-off-by: Tim Orling <tim.orling@konsulko.com>
---
 layerindex/tasks.py         |  6 ++--
 layerindex/update.py        | 30 +++++++++++++-----
 layerindex/views.py         |  2 +-
 tests/test_update_button.py | 62 +++++++++++++++++++++++++++++++++++++
 4 files changed, 90 insertions(+), 10 deletions(-)
 create mode 100644 tests/test_update_button.py
diff mbox series

Patch

diff --git a/layerindex/tasks.py b/layerindex/tasks.py
index 4af9466..b6aa0ae 100644
--- a/layerindex/tasks.py
+++ b/layerindex/tasks.py
@@ -42,7 +42,9 @@  def run_update_command(self, branch_name, update_command):
     from layerindex.models import Update
     updateobj = Update.objects.get(task_id=self.request.id)
     updateobj.started = datetime.now()
-    updateobj.save()
+    # Save only the fields set here: the update command may write to the
+    # same record (update.py --update)
+    updateobj.save(update_fields=['started'])
     output = ''
     shell = False
     if isinstance(update_command, str):
@@ -71,5 +73,5 @@  def run_update_command(self, branch_name, update_command):
         updateobj.log = output
         updateobj.finished = datetime.now()
         updateobj.retcode = retcode
-        updateobj.save()
+        updateobj.save(update_fields=['log', 'finished', 'retcode'])
     return {'retcode': retcode, 'output': erroutput}
diff --git a/layerindex/update.py b/layerindex/update.py
index ca68e3e..8a76084 100755
--- a/layerindex/update.py
+++ b/layerindex/update.py
@@ -183,6 +183,9 @@  def main():
     parser.add_option("-q", "--quiet",
             help = "Hide all output except error messages",
             action="store_const", const=logging.ERROR, dest="loglevel")
+    parser.add_option("-u", "--update",
+            help = "Record the update in this existing update record instead of creating a new one",
+            type="int", action="store", dest="update_id")
     parser.add_option("", "--keep-temp",
             help = "Preserve temporary directory at the end instead of deleting it",
             action="store_true")
@@ -267,14 +270,23 @@  def main():
     listhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
     logger.addHandler(listhandler)
 
-    update = Update()
-    update.started = datetime.now()
+    if options.update_id:
+        update = Update.objects.filter(id=options.update_id).first()
+        if not update:
+            logger.error("Specified update id %s does not exist in database" % options.update_id)
+            sys.exit(1)
+    else:
+        update = Update()
+        update.started = datetime.now()
     if options.fullreload or options.reload:
         update.reload = True
     else:
         update.reload = False
     if not options.dryrun:
-        update.save()
+        if options.update_id:
+            update.save(update_fields=['reload'])
+        else:
+            update.save()
     try:
         lockfn = os.path.join(fetchdir, "layerindex.lock")
         lockfile = utils.lock_file(lockfn, options.timeout, logger)
@@ -611,10 +623,14 @@  def main():
         logger.error(traceback.format_exc().rstrip())
         sys.exit(1)
     finally:
-        update.log = ''.join(listhandler.read())
-        update.finished = datetime.now()
-        if not options.dryrun:
-            update.save()
+        # With --update, whatever runs this command (e.g. the
+        # run_update_command task) records the log and when it finished,
+        # together with the exit status
+        if not options.update_id:
+            update.log = ''.join(listhandler.read())
+            update.finished = datetime.now()
+            if not options.dryrun:
+                update.save()
 
     if not options.dryrun:
         # Purge old update records
diff --git a/layerindex/views.py b/layerindex/views.py
index 592311f..9f238fd 100644
--- a/layerindex/views.py
+++ b/layerindex/views.py
@@ -147,7 +147,7 @@  def update_layer_view(request, template_name, branch='master', slug=None):
 
     from celery import uuid
 
-    cmd = 'layerindex/update.py --layer %s --branch %s;' % (layeritem.name, branch)
+    cmd = 'layerindex/update.py --layer %s --branch %s --update %%update%%;' % (layeritem.name, branch)
 
     task_id = uuid()
     # Create this here first, because inside the task we don't have all of the required info
diff --git a/tests/test_update_button.py b/tests/test_update_button.py
new file mode 100644
index 0000000..b518bc2
--- /dev/null
+++ b/tests/test_update_button.py
@@ -0,0 +1,62 @@ 
+# layerindex-web - tests for the Update Layer button and its task status page
+#
+# Licensed under the MIT license, see COPYING.MIT for details
+#
+# SPDX-License-Identifier: MIT
+
+from datetime import datetime
+
+import pytest
+from django.urls import reverse
+
+from layerindex import tasks, views
+from layerindex.models import Branch, LayerBranch, LayerItem, Update
+
+
+@pytest.fixture
+def staff_client(client, django_user_model):
+    user = django_user_model.objects.create_user('staff', 'staff@example.com', 'pw', is_staff=True)
+    client.force_login(user)
+    return client
+
+
+@pytest.mark.django_db
+def test_update_layer_creates_one_update(staff_client, monkeypatch):
+    branch, _ = Branch.objects.get_or_create(name='master', defaults={'bitbake_branch': 'master'})
+    layer = LayerItem.objects.create(name='meta-test', status='P', layer_type='A',
+                                     summary='test', description='test',
+                                     vcs_url='git://example.com/meta-test.git')
+    LayerBranch.objects.create(layer=layer, branch=branch)
+    queued = []
+    monkeypatch.setattr(views.tasks.run_update_command, 'apply_async',
+                        lambda args, task_id: queued.append((args, task_id)))
+    before = Update.objects.count()
+
+    response = staff_client.get(reverse('update_layer', args=('master', 'meta-test')))
+
+    assert response.status_code == 302
+    (branch_name, cmd), task_id = queued[0]
+    assert Update.objects.count() == before + 1
+    assert Update.objects.filter(task_id=task_id).exists()
+    # update.py records its work in the view's Update rather than a new one
+    assert '--update %update%' in cmd
+
+
+@pytest.mark.django_db
+def test_run_update_command_keeps_fields_set_by_command(settings, tmp_path, monkeypatch):
+    settings.TASK_LOG_DIR = str(tmp_path)
+    update = Update.objects.create(started=datetime.now(), task_id='test-task')
+
+    def fake_runcmd(cmd, destdir, outfile, shell):
+        # Stands in for update.py --update, writing to the same record
+        assert cmd == 'update.py --update %d' % update.id
+        Update.objects.filter(id=update.id).update(reload=True)
+        return 'output'
+    monkeypatch.setattr(tasks.utils, 'runcmd', fake_runcmd)
+
+    tasks.run_update_command.apply(('master', 'update.py --update %update%'), task_id='test-task')
+
+    update.refresh_from_db()
+    assert update.reload
+    assert (update.log, update.retcode) == ('output', 0)
+    assert update.finished is not None