[layerindex-web][PATCH 2/5] tests: add StatsView tests

Tim Orling <[email protected]>
Newsgroups org.yoctoproject.lists.yocto-patches
Message-ID <[email protected]>
Add test_stats_view.py covering the StatsView statistics page:

- HTTP 200 response
- perbranch context is a list of dicts (not a queryset)
- hidden branches are excluded
- branches are sorted by sort_priority
- each dict contains all keys the template requires
- per-branch counts match test fixture data
- updates_enabled is correctly reflected per branch
- overall context keys (layercount, *_count_distinct) are present

[YOCTO #15391]

AI-Generated: Claude Cowork Sonnet 4.6
Signed-off-by: Tim Orling <[email protected]>
---
 tests/test_stats_view.py | 164 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 164 insertions(+)
 create mode 100644 tests/test_stats_view.py

diff --git a/tests/test_stats_view.py b/tests/test_stats_view.py
new file mode 100644
index 0000000..c30cc17
--- /dev/null
+++ b/tests/test_stats_view.py
@@ -0,0 +1,164 @@
+# layerindex-web - tests for StatsView
+#
+# Copyright (C) 2026 Konsulko Group
+#
+# Licensed under the MIT license, see COPYING.MIT for details
+#
+# SPDX-License-Identifier: MIT
+
+# Tests for bug #15391 - Statistics page timeout
+# https://bugzilla.yoctoproject.org/show_bug.cgi?id=15391
+#
+# The fix replaces a single annotated queryset (which generated one huge SQL
+# query with multiple COUNT(DISTINCT ...) JOINs) with per-branch individual
+# COUNT queries that are much cheaper at production data volumes.
+
+import pytest
+from django.test import TestCase
+from django.urls import reverse
+
+from layerindex.models import (Branch, LayerItem, LayerBranch, Recipe,
+                                BBClass, Machine, Distro)
+
+
[email protected]_db
+class TestStatsView(TestCase):
+    """Tests for StatsView (bug #15391 - statistics page timeout fix)."""
+
+    def setUp(self):
+        # Create two visible branches with different sort priorities
+        self.branch1 = Branch.objects.create(
+            name='main',
+            bitbake_branch='master',
+            short_description='Main branch',
+            sort_priority=1,
+            hidden=False,
+            updates_enabled=True,
+        )
+        self.branch2 = Branch.objects.create(
+            name='wrynose',
+            bitbake_branch='2.18',
+            short_description='Wrynose branch',
+            sort_priority=2,
+            hidden=False,
+            updates_enabled=False,
+        )
+        # A hidden branch that should never appear in perbranch
+        self.branch_hidden = Branch.objects.create(
+            name='old-hidden',
+            bitbake_branch='1.0',
+            short_description='Old hidden branch',
+            sort_priority=99,
+            hidden=True,
+            updates_enabled=False,
+        )
+
+        # A layer
+        self.layer = LayerItem.objects.create(
+            name='meta-test',
+            status='P',
+            layer_type='A',
+            summary='Test layer',
+            description='A test layer',
+            vcs_url='git://example.com/meta-test.git',
+        )
+
+        # Wire up layerbranches
+        self.lb1 = LayerBranch.objects.create(layer=self.layer, branch=self.branch1)
+        self.lb2 = LayerBranch.objects.create(layer=self.layer, branch=self.branch2)
+
+        # Add objects only to branch1
+        Recipe.objects.create(
+            layerbranch=self.lb1,
+            filename='test_1.0.bb',
+            pn='test',
+            pv='1.0',
+            filepath='recipes-test',
+        )
+        BBClass.objects.create(
+            layerbranch=self.lb1,
+            name='testclass',
+        )
+        Machine.objects.create(
+            layerbranch=self.lb1,
+            name='qemux86',
+            description='QEMU x86 machine',
+        )
+        Distro.objects.create(
+            layerbranch=self.lb1,
+            name='testdistro',
+            description='Test distro',
+        )
+
+    def test_stats_view_returns_200(self):
+        """StatsView should return HTTP 200."""
+        response = self.client.get(reverse('stats'))
+        self.assertEqual(response.status_code, 200)
+
+    def test_perbranch_is_list_of_dicts(self):
+        """perbranch context should be a list of dicts, not a queryset."""
+        response = self.client.get(reverse('stats'))
+        perbranch = response.context['perbranch']
+        self.assertIsInstance(perbranch, list)
+        for item in perbranch:
+            self.assertIsInstance(item, dict)
+
+    def test_perbranch_excludes_hidden_branches(self):
+        """Hidden branches should not appear in perbranch."""
+        response = self.client.get(reverse('stats'))
+        names = [b['name'] for b in response.context['perbranch']]
+        self.assertIn('main', names)
+        self.assertIn('wrynose', names)
+        self.assertNotIn('old-hidden', names)
+
+    def test_perbranch_sorted_by_priority(self):
+        """Branches should be ordered by sort_priority."""
+        response = self.client.get(reverse('stats'))
+        names = [b['name'] for b in response.context['perbranch']]
+        self.assertEqual(names, ['master', 'main', 'wrynose'])
+
+    def test_perbranch_dict_has_required_keys(self):
+        """Each perbranch dict must contain all keys the template expects."""
+        required_keys = {'name', 'updates_enabled', 'layer_count',
+                         'recipe_count', 'class_count', 'machine_count', 'distro_count'}
+        response = self.client.get(reverse('stats'))
+        for item in response.context['perbranch']:
+            self.assertTrue(required_keys.issubset(item.keys()),
+                            f"Missing keys in perbranch item: {required_keys - item.keys()}")
+
+    def test_perbranch_counts_branch1(self):
+        """Per-branch counts for branch1 should reflect the test data."""
+        response = self.client.get(reverse('stats'))
+        branch1_data = next(b for b in response.context['perbranch'] if b['name'] == 'main')
+        self.assertEqual(branch1_data['layer_count'], 1)
+        self.assertEqual(branch1_data['recipe_count'], 1)
+        self.assertEqual(branch1_data['class_count'], 1)
+        self.assertEqual(branch1_data['machine_count'], 1)
+        self.assertEqual(branch1_data['distro_count'], 1)
+    
+    def test_perbranch_counts_branch2_empty(self):
+        """Branch2 has a layerbranch but no recipes/classes/machines/distros."""
+        response = self.client.get(reverse('stats'))
+        branch2_data = next(b for b in response.context['perbranch'] if b['name'] == 'wrynose')
+        self.assertEqual(branch2_data['layer_count'], 1)
+        self.assertEqual(branch2_data['recipe_count'], 0)
+        self.assertEqual(branch2_data['class_count'], 0)
+        self.assertEqual(branch2_data['machine_count'], 0)
+        self.assertEqual(branch2_data['distro_count'], 0)
+
+    def test_perbranch_updates_enabled_field(self):
+        """updates_enabled should be correctly reflected per branch."""
+        response = self.client.get(reverse('stats'))
+        perbranch = response.context['perbranch']
+        b1 = next(b for b in perbranch if b['name'] == 'main')
+        b2 = next(b for b in perbranch if b['name'] == 'wrynose')
+        self.assertTrue(b1['updates_enabled'])
+        self.assertFalse(b2['updates_enabled'])
+
+    def test_overall_context_keys_present(self):
+        """Overall statistics context keys should all be present."""
+        response = self.client.get(reverse('stats'))
+        for key in ('layercount', 'recipe_count_distinct', 'class_count_distinct',
+                    'machine_count_distinct', 'distro_count_distinct'):
+            self.assertIn(key, response.context,
+                          f"Missing context key: {key}")
-- 
2.54.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.