Skip to content

Commit 701269c

Browse files
committed
test(spanner): speed up unit test suite and eliminate idle retry sleeps
Speed up the unit test suite by ~55% (reducing full suite runtime from ~66s to ~29s) by eliminating wall-clock sleep delays, avoiding live gRPC transport initialization, and reducing cryptographic CPU burn in protocol tests. Key changes: - Intercept retry backoff sleeps: Mock localized `time.sleep` and `asyncio.sleep` in transaction, snapshot, and batch commit retry tests. Each test now verifies retry counts and delay values without idling in real time. - Deterministic concurrency synchronization: Replace arbitrary timing sleeps in session manager and metrics tests with explicit `asyncio.Event` and `threading.Barrier` coordination, eliminating CI flakiness under CPU contention. - Eliminate teardown polling: Remove `_assert_true_with_timeout` and sleep polling from `test_database_session_manager.py` by relying on event-driven rotation callbacks and blocking thread joins. - Isolate GAPIC admin transports: Use autospec mock transports in `test_client.py` and `test_instance.py` instead of default credentials, eliminating live gRPC channel creation and 60-second network timeout risks. - Lower Omni Argon2id test parameters: Use minimal valid test parameters (1 iteration, 8 KB RAM) in protocol flow and state machine tests, reducing key derivation time by ~1,100x while leaving RFC golden vector tests untouched. - Clean up legacy mocks: Convert legacy global `mock.patch("time.sleep")` calls in `test_session.py` to localized module paths adhering to repository mock hygiene rules.
1 parent 37123e5 commit 701269c

12 files changed

Lines changed: 169 additions & 110 deletions

‎packages/google-cloud-spanner/tests/unit/_async/test_session.py‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,9 +1219,13 @@ async def unit_of_work(transaction):
12191219
pass
12201220

12211221
await session.create()
1222-
await session.run_in_transaction(unit_of_work)
1222+
with mock.patch(
1223+
"google.cloud.spanner_v1._async._helpers.asyncio.sleep"
1224+
) as sleep_mock:
1225+
await session.run_in_transaction(unit_of_work)
12231226

12241227
self.assertEqual(begin_transaction.call_count, 2)
1228+
sleep_mock.assert_called_once()
12251229

12261230
begin_transaction.assert_called_with(
12271231
request=BeginTransactionRequest(
@@ -1261,10 +1265,14 @@ async def unit_of_work(transaction):
12611265
pass
12621266

12631267
await session.create()
1264-
await session.run_in_transaction(unit_of_work)
1268+
with mock.patch(
1269+
"google.cloud.spanner_v1._async._helpers.asyncio.sleep"
1270+
) as sleep_mock:
1271+
await session.run_in_transaction(unit_of_work)
12651272

12661273
# Verify retried BeginTransaction API call.
12671274
self.assertEqual(begin_transaction.call_count, 2)
1275+
sleep_mock.assert_called_once()
12681276

12691277
begin_transaction.assert_called_with(
12701278
request=BeginTransactionRequest(
@@ -1308,10 +1316,14 @@ async def unit_of_work(transaction):
13081316
pass
13091317

13101318
await session.create()
1311-
await session.run_in_transaction(unit_of_work)
1319+
with mock.patch(
1320+
"google.cloud.spanner_v1._async._helpers.asyncio.sleep"
1321+
) as sleep_mock:
1322+
await session.run_in_transaction(unit_of_work)
13121323

13131324
# Verify retried BeginTransaction API call.
13141325
self.assertEqual(begin_transaction.call_count, 2)
1326+
sleep_mock.assert_called_once()
13151327

13161328
begin_transaction.assert_called_with(
13171329
request=BeginTransactionRequest(

‎packages/google-cloud-spanner/tests/unit/_async/test_snapshot.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1367,9 +1367,14 @@ async def test_begin_transaction_retry(self):
13671367
TransactionPB(id=TXN_ID),
13681368
]
13691369

1370-
tid = await snapshot._begin_transaction()
1370+
with mock.patch(
1371+
"google.cloud.spanner_v1._async._helpers.asyncio.sleep"
1372+
) as sleep_mock:
1373+
tid = await snapshot._begin_transaction()
1374+
13711375
self.assertEqual(tid, TXN_ID)
13721376
self.assertEqual(api.begin_transaction.call_count, 2)
1377+
sleep_mock.assert_called_once_with(2)
13731378

13741379
async def test_update_for_transaction_pb_w_precommit_token(self):
13751380
from google.cloud.spanner_v1.types import MultiplexedSessionPrecommitToken

‎packages/google-cloud-spanner/tests/unit/_async/test_transaction_extra.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,13 @@ async def test_commit_retry_and_precommit_token(self):
135135
final_resp,
136136
]
137137

138-
await txn.commit()
138+
with mock.patch(
139+
"google.cloud.spanner_v1._async._helpers.asyncio.sleep"
140+
) as sleep_mock:
141+
await txn.commit()
142+
139143
self.assertEqual(self.db.spanner_api.commit.call_count, 3)
144+
sleep_mock.assert_called_once_with(2)
140145

141146
async def test_execute_update_request_options_dict(self):
142147
# coverage for line 503

‎packages/google-cloud-spanner/tests/unit/omni/test_login_client.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ def test_login_successful_flow(self):
4141

4242
params = authentication_pb2.HashParameters(
4343
argon2_id_parameters=authentication_pb2.HashParameters.Argon2IdParameters(
44-
iteration_count=3,
45-
memory_usage=64 * 1024,
46-
parallelism=4,
44+
iteration_count=1,
45+
memory_usage=8,
46+
parallelism=1,
4747
hash_size=32,
4848
)
4949
)
@@ -248,9 +248,9 @@ def test_login_missing_access_token_in_final_response(self):
248248
password = "test_password"
249249
params = authentication_pb2.HashParameters(
250250
argon2_id_parameters=authentication_pb2.HashParameters.Argon2IdParameters(
251-
iteration_count=3,
252-
memory_usage=64 * 1024,
253-
parallelism=4,
251+
iteration_count=1,
252+
memory_usage=8,
253+
parallelism=1,
254254
hash_size=32,
255255
)
256256
)

‎packages/google-cloud-spanner/tests/unit/omni/test_opaque.py‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -486,9 +486,9 @@ def test_oprf_evaluate(self):
486486
def test_authenticator_validation(self):
487487
valid_params = authentication_pb2.HashParameters(
488488
argon2_id_parameters=authentication_pb2.HashParameters.Argon2IdParameters(
489-
iteration_count=3,
490-
memory_usage=64 * 1024,
491-
parallelism=4,
489+
iteration_count=1,
490+
memory_usage=8,
491+
parallelism=1,
492492
hash_size=32,
493493
)
494494
)
@@ -512,9 +512,9 @@ def test_authenticator_validation(self):
512512
def test_authenticator_state_errors(self):
513513
params = authentication_pb2.HashParameters(
514514
argon2_id_parameters=authentication_pb2.HashParameters.Argon2IdParameters(
515-
iteration_count=3,
516-
memory_usage=64 * 1024,
517-
parallelism=4,
515+
iteration_count=1,
516+
memory_usage=8,
517+
parallelism=1,
518518
hash_size=32,
519519
)
520520
)
@@ -541,9 +541,9 @@ def test_authenticator_state_errors(self):
541541
def test_user_authenticator_clear_and_del(self):
542542
params = authentication_pb2.HashParameters(
543543
argon2_id_parameters=authentication_pb2.HashParameters.Argon2IdParameters(
544-
iteration_count=3,
545-
memory_usage=64 * 1024,
546-
parallelism=4,
544+
iteration_count=1,
545+
memory_usage=8,
546+
parallelism=1,
547547
hash_size=32,
548548
)
549549
)
@@ -584,9 +584,9 @@ def test_full_opaque_handshake_simulation(self):
584584

585585
params = authentication_pb2.HashParameters(
586586
argon2_id_parameters=authentication_pb2.HashParameters.Argon2IdParameters(
587-
iteration_count=3,
588-
memory_usage=64 * 1024,
589-
parallelism=4,
587+
iteration_count=1,
588+
memory_usage=8,
589+
parallelism=1,
590590
hash_size=32,
591591
)
592592
)
@@ -708,9 +708,9 @@ def test_full_opaque_handshake_simulation(self):
708708
def test_final_request_invalid_masked_response_length(self):
709709
params = authentication_pb2.HashParameters(
710710
argon2_id_parameters=authentication_pb2.HashParameters.Argon2IdParameters(
711-
iteration_count=3,
712-
memory_usage=64 * 1024,
713-
parallelism=4,
711+
iteration_count=1,
712+
memory_usage=8,
713+
parallelism=1,
714714
hash_size=32,
715715
)
716716
)
@@ -964,9 +964,9 @@ def test_final_request_edge_cases(self):
964964

965965
params = authentication_pb2.HashParameters(
966966
argon2_id_parameters=authentication_pb2.HashParameters.Argon2IdParameters(
967-
iteration_count=3,
968-
memory_usage=64 * 1024,
969-
parallelism=4,
967+
iteration_count=1,
968+
memory_usage=8,
969+
parallelism=1,
970970
hash_size=32,
971971
)
972972
)

‎packages/google-cloud-spanner/tests/unit/test_batch.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,20 @@ def test_aborted_exception_on_commit_with_retries(self, mock_region):
345345
batch.insert(TABLE_NAME, COLUMNS, VALUES)
346346

347347
# Assertion: Ensure that calling batch.commit() raises Aborted
348-
with self.assertRaises(Aborted) as context:
349-
batch.commit(timeout_secs=1.0, default_retry_delay=0)
348+
delay_call_count = 0
349+
350+
def fake_delay(exc, deadline, attempts, default_retry_delay=None):
351+
nonlocal delay_call_count
352+
delay_call_count += 1
353+
if delay_call_count >= 2:
354+
raise exc
355+
356+
with mock.patch(
357+
"google.cloud.spanner_v1._helpers._delay_until_retry",
358+
side_effect=fake_delay,
359+
):
360+
with self.assertRaises(Aborted) as context:
361+
batch.commit(timeout_secs=1.0, default_retry_delay=0)
350362

351363
# Verify exception includes request_id attribute
352364
self.assertIn("409 Transaction was aborted", str(context.exception))

‎packages/google-cloud-spanner/tests/unit/test_client.py‎

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ def _get_target_class(self):
5959
def _make_one(self, *args, **kwargs):
6060
return self._get_target_class()(*args, **kwargs)
6161

62+
@staticmethod
63+
def _make_instance_admin_api():
64+
from google.cloud.spanner_admin_instance_v1 import InstanceAdminClient
65+
from google.cloud.spanner_admin_instance_v1.services.instance_admin.transports.base import (
66+
InstanceAdminTransport,
67+
)
68+
69+
mock_transport = mock.create_autospec(InstanceAdminTransport, instance=True)
70+
mock_transport._wrapped_methods = {}
71+
return InstanceAdminClient(transport=mock_transport)
72+
6273
def _constructor_test_helper(
6374
self,
6475
expected_scopes,
@@ -627,15 +638,14 @@ def test_project_name_property(self):
627638

628639
def test_list_instance_configs(self):
629640
from google.cloud.spanner_admin_instance_v1 import (
630-
InstanceAdminClient,
631-
ListInstanceConfigsRequest,
632-
ListInstanceConfigsResponse,
641+
InstanceConfig as InstanceConfigPB,
633642
)
634643
from google.cloud.spanner_admin_instance_v1 import (
635-
InstanceConfig as InstanceConfigPB,
644+
ListInstanceConfigsRequest,
645+
ListInstanceConfigsResponse,
636646
)
637647

638-
api = InstanceAdminClient(credentials=AnonymousCredentials())
648+
api = self._make_instance_admin_api()
639649
credentials = build_scoped_credentials()
640650
client = self._make_one(project=self.PROJECT, credentials=credentials)
641651
client._instance_admin_api = api
@@ -676,16 +686,15 @@ def test_list_instance_configs(self):
676686

677687
def test_list_instance_configs_w_options(self):
678688
from google.cloud.spanner_admin_instance_v1 import (
679-
InstanceAdminClient,
680-
ListInstanceConfigsRequest,
681-
ListInstanceConfigsResponse,
689+
InstanceConfig as InstanceConfigPB,
682690
)
683691
from google.cloud.spanner_admin_instance_v1 import (
684-
InstanceConfig as InstanceConfigPB,
692+
ListInstanceConfigsRequest,
693+
ListInstanceConfigsResponse,
685694
)
686695

687696
credentials = build_scoped_credentials()
688-
api = InstanceAdminClient(credentials=credentials)
697+
api = self._make_instance_admin_api()
689698
client = self._make_one(project=self.PROJECT, credentials=credentials)
690699
client._instance_admin_api = api
691700

@@ -756,13 +765,12 @@ def test_instance_factory_explicit(self):
756765
def test_list_instances(self):
757766
from google.cloud.spanner_admin_instance_v1 import Instance as InstancePB
758767
from google.cloud.spanner_admin_instance_v1 import (
759-
InstanceAdminClient,
760768
ListInstancesRequest,
761769
ListInstancesResponse,
762770
)
763771

772+
api = self._make_instance_admin_api()
764773
credentials = build_scoped_credentials()
765-
api = InstanceAdminClient(credentials=credentials)
766774
client = self._make_one(project=self.PROJECT, credentials=credentials)
767775
client._instance_admin_api = api
768776

@@ -806,13 +814,12 @@ def test_list_instances(self):
806814

807815
def test_list_instances_w_options(self):
808816
from google.cloud.spanner_admin_instance_v1 import (
809-
InstanceAdminClient,
810817
ListInstancesRequest,
811818
ListInstancesResponse,
812819
)
813820

821+
api = self._make_instance_admin_api()
814822
credentials = build_scoped_credentials()
815-
api = InstanceAdminClient(credentials=credentials)
816823
client = self._make_one(project=self.PROJECT, credentials=credentials)
817824
client._instance_admin_api = api
818825

0 commit comments

Comments
 (0)