transaction_tests: Remove testing URL.

Rewrite the test so that we don't have a dedicated URL for testing.
dev_update_subgroups is called directly from the tests without using the
test client.
This commit is contained in:
Zixuan James Li 2023-09-01 12:14:21 -10:00 committed by Tim Abbott
parent 81bd63cb46
commit 1e1f98edb2
3 changed files with 63 additions and 92 deletions

View File

@ -1,16 +1,63 @@
import threading import threading
from typing import TYPE_CHECKING, List, Optional from typing import Any, List, Optional
from unittest import mock
import orjson import orjson
from django.db import connections, transaction from django.db import OperationalError, connections, transaction
from django.http import HttpRequest
from zerver.actions.user_groups import add_subgroups_to_user_group, check_add_user_group from zerver.actions.user_groups import add_subgroups_to_user_group, check_add_user_group
from zerver.lib.exceptions import JsonableError
from zerver.lib.test_classes import ZulipTransactionTestCase from zerver.lib.test_classes import ZulipTransactionTestCase
from zerver.models import Realm, UserGroup, get_realm from zerver.lib.test_helpers import HostRequestMock
from zerver.views.development import user_groups as user_group_view from zerver.lib.user_groups import access_user_group_by_id
from zerver.models import Realm, UserGroup, UserProfile, get_realm
from zerver.views.user_groups import update_subgroups_of_user_group
if TYPE_CHECKING: BARRIER: Optional[threading.Barrier] = None
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
def dev_update_subgroups(
request: HttpRequest,
user_profile: UserProfile,
user_group_id: int,
) -> Optional[str]:
# The test is expected to set up the barrier before accessing this endpoint.
assert BARRIER is not None
try:
with transaction.atomic(), mock.patch(
"zerver.lib.user_groups.access_user_group_by_id"
) as m:
def wait_after_recursive_query(*args: Any, **kwargs: Any) -> UserGroup:
# When updating the subgroups, we access the supergroup group
# only after finishing the recursive query.
BARRIER.wait()
return access_user_group_by_id(*args, **kwargs)
m.side_effect = wait_after_recursive_query
update_subgroups_of_user_group(request, user_profile, user_group_id=user_group_id)
except OperationalError as err:
msg = str(err)
if "deadlock detected" in msg:
return "Deadlock detected"
else:
assert "could not obtain lock" in msg
# This error is possible when nowait is set the True, which only
# applies to the recursive query on the subgroups. Because the
# recursive query fails, this thread must have not waited on the
# barrier yet.
BARRIER.wait()
return "Busy lock detected"
except (
threading.BrokenBarrierError
): # nocoverage # This is only possible when timeout happens or there is a programming error
raise JsonableError(
"Broken barrier. The tester should make sure that the exact number of parties have waited on the barrier set by the previous immediate set_sync_after_first_lock call"
)
return None
class UserGroupRaceConditionTestCase(ZulipTransactionTestCase): class UserGroupRaceConditionTestCase(ZulipTransactionTestCase):
@ -46,7 +93,7 @@ class UserGroupRaceConditionTestCase(ZulipTransactionTestCase):
def test_lock_subgroups_with_respect_to_supergroup(self) -> None: def test_lock_subgroups_with_respect_to_supergroup(self) -> None:
realm = get_realm("zulip") realm = get_realm("zulip")
self.login("iago") self.login("iago")
test_case = self iago = self.example_user("iago")
class RacingThread(threading.Thread): class RacingThread(threading.Thread):
def __init__( def __init__(
@ -55,15 +102,16 @@ class UserGroupRaceConditionTestCase(ZulipTransactionTestCase):
supergroup_id: int, supergroup_id: int,
) -> None: ) -> None:
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.response: Optional["TestHttpResponse"] = None self.response: Optional[str] = None
self.subgroup_ids = subgroup_ids self.subgroup_ids = subgroup_ids
self.supergroup_id = supergroup_id self.supergroup_id = supergroup_id
def run(self) -> None: def run(self) -> None:
try: try:
self.response = test_case.client_post( self.response = dev_update_subgroups(
url=f"/testing/user_groups/{self.supergroup_id}/subgroups", HostRequestMock({"add": orjson.dumps(self.subgroup_ids).decode()}),
info={"add": orjson.dumps(self.subgroup_ids).decode()}, iago,
user_group_id=self.supergroup_id,
) )
finally: finally:
# Close all thread-local database connections # Close all thread-local database connections
@ -81,9 +129,8 @@ real subgroup update endpoint by synchronizing them after the acquisition of the
first lock in the critical region. Though unlikely, this test might fail as we first lock in the critical region. Though unlikely, this test might fail as we
have no control over the scheduler when the barrier timeouts. have no control over the scheduler when the barrier timeouts.
""".strip() """.strip()
barrier = threading.Barrier(parties=2, timeout=3) global BARRIER
BARRIER = threading.Barrier(parties=2, timeout=3)
user_group_view.set_sync_after_recursive_query(barrier)
t1.start() t1.start()
t2.start() t2.start()
@ -91,12 +138,11 @@ have no control over the scheduler when the barrier timeouts.
for t in [t1, t2]: for t in [t1, t2]:
t.join() t.join()
response = t.response response = t.response
if response is not None and response.status_code == 200: if response is None:
succeeded += 1 succeeded += 1
continue continue
assert response is not None self.assertEqual(response, error_messsage)
self.assert_json_error(response, error_messsage)
# Race condition resolution should only allow one thread to succeed # Race condition resolution should only allow one thread to succeed
self.assertEqual( self.assertEqual(
succeeded, succeeded,

View File

@ -1,65 +0,0 @@
import threading
from typing import Any, Optional
from unittest import mock
from django.db import OperationalError, transaction
from django.http import HttpRequest, HttpResponse
from zerver.lib.exceptions import JsonableError
from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success
from zerver.lib.user_groups import access_user_group_by_id
from zerver.lib.validator import check_int
from zerver.models import UserGroup, UserProfile
from zerver.views.user_groups import update_subgroups_of_user_group
BARRIER: Optional[threading.Barrier] = None
def set_sync_after_recursive_query(barrier: Optional[threading.Barrier]) -> None:
global BARRIER
BARRIER = barrier
@has_request_variables
def dev_update_subgroups(
request: HttpRequest,
user_profile: UserProfile,
user_group_id: int = REQ(json_validator=check_int, path_only=True),
) -> HttpResponse:
# The test is expected to set up the barrier before accessing this endpoint.
assert BARRIER is not None
try:
with transaction.atomic(), mock.patch(
"zerver.lib.user_groups.access_user_group_by_id"
) as m:
def wait_after_recursive_query(*args: Any, **kwargs: Any) -> UserGroup:
# When updating the subgroups, we access the supergroup group
# only after finishing the recursive query.
BARRIER.wait()
return access_user_group_by_id(*args, **kwargs)
m.side_effect = wait_after_recursive_query
update_subgroups_of_user_group(request, user_profile, user_group_id=user_group_id)
except OperationalError as err:
msg = str(err)
if "deadlock detected" in msg:
raise JsonableError("Deadlock detected")
else:
assert "could not obtain lock" in msg
# This error is possible when nowait is set the True, which only
# applies to the recursive query on the subgroups. Because the
# recursive query fails, this thread must have not waited on the
# barrier yet.
BARRIER.wait()
raise JsonableError("Busy lock detected")
except (
threading.BrokenBarrierError
): # nocoverage # This is only possible when timeout happens or there is a programming error
raise JsonableError(
"Broken barrier. The tester should make sure that the exact number of parties have waited on the barrier set by the previous immediate set_sync_after_first_lock call"
)
return json_success(request)

View File

@ -10,7 +10,6 @@ from django.urls import path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.static import serve from django.views.static import serve
from zerver.lib.rest import rest_path
from zerver.views.auth import config_error, login_page from zerver.views.auth import config_error, login_page
from zerver.views.development.cache import remove_caches from zerver.views.development.cache import remove_caches
from zerver.views.development.camo import handle_camo_url from zerver.views.development.camo import handle_camo_url
@ -32,7 +31,6 @@ from zerver.views.development.registration import (
register_development_realm, register_development_realm,
register_development_user, register_development_user,
) )
from zerver.views.development.user_groups import dev_update_subgroups
# These URLs are available only in the development environment # These URLs are available only in the development environment
@ -100,14 +98,6 @@ urls = [
path("external_content/<digest>/<received_url>", handle_camo_url), path("external_content/<digest>/<received_url>", handle_camo_url),
] ]
testing_urls = [
rest_path(
"testing/user_groups/<int:user_group_id>/subgroups",
POST=(dev_update_subgroups, {"intentionally_undocumented"}),
),
]
urls += testing_urls
v1_api_mobile_patterns = [ v1_api_mobile_patterns = [
# This is for the signing in through the devAuthBackEnd on mobile apps. # This is for the signing in through the devAuthBackEnd on mobile apps.
path("dev_fetch_api_key", api_dev_fetch_api_key), path("dev_fetch_api_key", api_dev_fetch_api_key),