[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