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