Bladeren bron

chore(tests): Allow skipping query-count checks (#23456)

Add SKIP_QUERY_COUNTS=1, which runs the blocks wrapped by
assert_expected_query_count() without a baseline lookup or count check.
Only the value 1 enables it, and combining it with UPDATE_QUERY_COUNTS
raises an error.

Fixes #23452
Martin Hauser 2 dagen geleden
bovenliggende
commit
30e9e056c7

+ 8 - 0
docs/development/getting-started.md

@@ -198,6 +198,14 @@ UPDATE_QUERY_COUNTS=1 python manage.py test --keepdb
 
 `UPDATE_QUERY_COUNTS` mode requires serial execution; do not combine it with `--parallel`. You can target a single test, app, or the full suite — only the keys exercised by the run are updated. Review the resulting diff in the JSON files as part of the PR; a reviewer should be able to see and reason about every query-count change.
 
+To run the tests without enforcing the baselines, set `SKIP_QUERY_COUNTS=1`. The tests still make their requests and run their functional assertions, but the baselines are not read and the query counts are not checked against them. This helps when the same tests run against several NetBox versions, such as in a plugin's CI matrix, because query counts can differ between releases even when the tested code is unchanged. Enforce the baselines against one reference version and skip them on the others.
+
+```no-highlight
+SKIP_QUERY_COUNTS=1 python manage.py test --keepdb
+```
+
+Only the value `1` enables this mode. Unlike `UPDATE_QUERY_COUNTS` mode, it works with `--parallel`. Combining it with `UPDATE_QUERY_COUNTS` raises an error.
+
 ## Submitting Pull Requests
 
 !!! note "Vouched contributors"

+ 17 - 0
netbox/utilities/testing/query_counts.py

@@ -19,6 +19,7 @@ __all__ = (
 )
 
 UPDATE_ENV_VAR = 'UPDATE_QUERY_COUNTS'
+SKIP_ENV_VAR = 'SKIP_QUERY_COUNTS'
 BASELINE_FILENAME = 'query_counts.json'
 
 _loaded_baselines = {}
@@ -29,6 +30,11 @@ def _is_update_mode():
     return bool(os.environ.get(UPDATE_ENV_VAR))
 
 
+def _is_skip_mode():
+    # Stricter than _is_update_mode(), so "0" or "false" cannot silently disable the checks
+    return os.environ.get(SKIP_ENV_VAR) == '1'
+
+
 def _is_parallel_test_run():
     # Heuristic: inspects sys.argv for Django's --parallel flag. This is
     # sufficient for the project's standard `manage.py test` invocations but
@@ -98,7 +104,18 @@ def assert_expected_query_count(test_case, name):
     When the `UPDATE_QUERY_COUNTS` environment variable is set, the assertion
     is skipped and the observed count is written back to the baseline file
     immediately. Update mode requires serial test execution (no --parallel).
+
+    When `SKIP_QUERY_COUNTS` is set to 1, the block runs with no baseline lookup
+    or count check. It cannot be combined with `UPDATE_QUERY_COUNTS`.
     """
+    if _is_skip_mode():
+        if _is_update_mode():
+            raise RuntimeError(
+                f"{SKIP_ENV_VAR}=1 cannot be combined with {UPDATE_ENV_VAR}. Unset one of them."
+            )
+        yield
+        return
+
     model = test_case.model
     app_label = model._meta.app_label
     label = getattr(test_case, 'query_count_model_label', None)

+ 50 - 11
netbox/utilities/tests/test_query_counts.py

@@ -1,4 +1,5 @@
 import os
+from unittest.mock import patch
 
 from django.test import TestCase
 
@@ -19,10 +20,9 @@ class _BaseTestCase:
     model = _FakeModel
 
 
-class AssertExpectedQueryCountLabelTestCase(TestCase):
+class AssertExpectedQueryCountTestCase(TestCase):
     """
-    Verify that assert_expected_query_count uses query_count_model_label when
-    set on the test case and falls back to model._meta.model_name otherwise.
+    Verify the baseline keys and the check modes of assert_expected_query_count.
     """
 
     def _make_test_case(self, label_value=None, set_attr=False):
@@ -49,16 +49,9 @@ class AssertExpectedQueryCountLabelTestCase(TestCase):
         qc_mod._record_update = fake_record
         qc_mod._is_parallel_test_run = lambda: False
         try:
-            old_env = os.environ.get(qc_mod.UPDATE_ENV_VAR)
-            os.environ[qc_mod.UPDATE_ENV_VAR] = '1'
-            try:
+            with patch.dict(os.environ, {'UPDATE_QUERY_COUNTS': '1', 'SKIP_QUERY_COUNTS': ''}):
                 with assert_expected_query_count(test_case, 'test_name'):
                     pass
-            finally:
-                if old_env is None:
-                    del os.environ[qc_mod.UPDATE_ENV_VAR]
-                else:
-                    os.environ[qc_mod.UPDATE_ENV_VAR] = old_env
         finally:
             qc_mod._record_update = original
             qc_mod._is_parallel_test_run = original_parallel
@@ -82,3 +75,49 @@ class AssertExpectedQueryCountLabelTestCase(TestCase):
         tc = self._make_test_case(label_value='', set_attr=True)
         key = self._recorded_key(tc)
         self.assertEqual(key, ':test_name')
+
+    def test_checks_enabled_by_default(self):
+        """A count mismatch fails unless SKIP_QUERY_COUNTS is 1."""
+        tc = self._make_test_case()
+        with (
+            patch.dict(os.environ, {'UPDATE_QUERY_COUNTS': ''}),
+            patch.object(qc_mod, '_load_baseline', return_value={'fakemodel:test_name': 99}),
+        ):
+            os.environ.pop('SKIP_QUERY_COUNTS', None)
+            with self.assertRaisesRegex(AssertionError, 'expected 99, got 0'):
+                with assert_expected_query_count(tc, 'test_name'):
+                    pass
+            os.environ['SKIP_QUERY_COUNTS'] = '0'
+            with self.assertRaisesRegex(AssertionError, 'expected 99, got 0'):
+                with assert_expected_query_count(tc, 'test_name'):
+                    pass
+
+    def test_skip_mode_bypasses_checks(self):
+        """SKIP_QUERY_COUNTS=1 runs the block unchecked, also for a missing key and under --parallel."""
+        tc = self._make_test_case()
+        ran = []
+        with (
+            patch.dict(os.environ, {'UPDATE_QUERY_COUNTS': '', 'SKIP_QUERY_COUNTS': '1'}),
+            patch.object(qc_mod, '_load_baseline', return_value={'fakemodel:test_name': 99}) as load_baseline,
+            patch.object(qc_mod, '_is_parallel_test_run', return_value=True),
+        ):
+            with assert_expected_query_count(tc, 'test_name'):
+                ran.append('test_name')
+            with assert_expected_query_count(tc, 'unrecorded'):
+                ran.append('unrecorded')
+        self.assertEqual(ran, ['test_name', 'unrecorded'])
+        load_baseline.assert_not_called()
+
+    def test_skip_mode_rejects_update_mode(self):
+        """Combining SKIP_QUERY_COUNTS and UPDATE_QUERY_COUNTS raises before the block runs."""
+        tc = self._make_test_case()
+        ran = []
+        with (
+            patch.dict(os.environ, {'UPDATE_QUERY_COUNTS': '1', 'SKIP_QUERY_COUNTS': '1'}),
+            patch.object(qc_mod, '_record_update') as record_update,
+        ):
+            with self.assertRaisesRegex(RuntimeError, 'SKIP_QUERY_COUNTS=1 cannot be combined'):
+                with assert_expected_query_count(tc, 'test_name'):
+                    ran.append('test_name')
+        self.assertEqual(ran, [])
+        record_update.assert_not_called()