Skip to content

Commit f006770

Browse files
authored
chore(firestore): optimize system tests runtime (#17418)
Optimizes Firestore system tests execution time to 13 minutes (down from ~1 hour) under live GCP environments. **Parallelism:** Added pytest-xdist using all cores (--dist load for maximum load balancing). **Isolation:** Appended os.getpid() to test resource IDs to prevent parallel worker collisions. **Fixture Re-use:** Promoted query setup fixtures to module scope to reduce database setups by ~97%. **Fast Polling:** Reduced hardcoded delays in watch tests from 1.0s to 0.2s with early-exit polling.
1 parent 6547012 commit f006770

5 files changed

Lines changed: 93 additions & 20 deletions

File tree

.librarian/generator-input/client-post-processing/firestore-integration.yaml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,9 @@ replacements:
564564
"freezegun",
565565
]
566566
count: 1
567+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17429):
568+
# Temporary post-processing rule to add pytest-xdist dependency.
569+
# Remove this once gapic-generator includes pytest-xdist by default.
567570
- paths: [
568571
packages/google-cloud-firestore/noxfile.py
569572
]
@@ -574,6 +577,7 @@ replacements:
574577
"pytest-asyncio",
575578
"six",
576579
"pyyaml",
580+
"pytest-xdist",
577581
]
578582
count: 1
579583
- paths: [
@@ -584,6 +588,54 @@ replacements:
584588
after: |
585589
"pytest-asyncio==0.21.2",
586590
count: 2
591+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17429):
592+
# Temporary post-processing rule to inject `-n auto` for Firestore parallel tests.
593+
# This rule should be removed once the generator template changes are released
594+
# and the generator version is updated in librarian.yaml.
595+
- paths: [
596+
packages/google-cloud-firestore/noxfile.py
597+
]
598+
before: |
599+
# Run py.test against the system tests\.
600+
if system_test_exists:
601+
session\.run\(
602+
"py.test",
603+
"--quiet",
604+
f"--junitxml=system_\{session\.python\}_sponge_log\.xml",
605+
system_test_path,
606+
\*session\.posargs,
607+
\)
608+
if system_test_folder_exists:
609+
session\.run\(
610+
"py.test",
611+
"--quiet",
612+
f"--junitxml=system_\{session\.python\}_sponge_log\.xml",
613+
system_test_folder_path,
614+
\*session\.posargs,
615+
\)
616+
after: |
617+
# Run py.test against the system tests.
618+
if system_test_exists:
619+
session.run(
620+
"py.test",
621+
"-n",
622+
"auto",
623+
"--quiet",
624+
f"--junitxml=system_{session.python}_sponge_log.xml",
625+
system_test_path,
626+
*session.posargs,
627+
)
628+
if system_test_folder_exists:
629+
session.run(
630+
"py.test",
631+
"-n",
632+
"auto",
633+
"--quiet",
634+
f"--junitxml=system_{session.python}_sponge_log.xml",
635+
system_test_folder_path,
636+
*session.posargs,
637+
)
638+
count: 1
587639
- paths: [
588640
"packages/google-cloud-firestore/docs/conf.py",
589641
]

packages/google-cloud-firestore/noxfile.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"pytest-asyncio==0.21.2",
8383
"six",
8484
"pyyaml",
85+
"pytest-xdist",
8586
]
8687
SYSTEM_TEST_LOCAL_DEPENDENCIES: List[str] = []
8788
SYSTEM_TEST_DEPENDENCIES: List[str] = []
@@ -402,6 +403,8 @@ def system(session):
402403
if system_test_exists:
403404
session.run(
404405
"py.test",
406+
"-n",
407+
"auto",
405408
"--quiet",
406409
f"--junitxml=system_{session.python}_sponge_log.xml",
407410
system_test_path,
@@ -410,6 +413,8 @@ def system(session):
410413
if system_test_folder_exists:
411414
session.run(
412415
"py.test",
416+
"-n",
417+
"auto",
413418
"--quiet",
414419
f"--junitxml=system_{session.python}_sponge_log.xml",
415420
system_test_folder_path,

packages/google-cloud-firestore/tests/system/test__helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
MISSING_DOCUMENT = "No document to update: "
1717
DOCUMENT_EXISTS = "Document already exists: "
1818
ENTERPRISE_MODE_ERROR = "only allowed on ENTERPRISE mode"
19-
UNIQUE_RESOURCE_ID = unique_resource_id("-")
19+
UNIQUE_RESOURCE_ID = unique_resource_id("-") + "-" + str(os.getpid())
2020
EMULATOR_CREDS = EmulatorCreds()
2121
FIRESTORE_EMULATOR = os.environ.get(_FIRESTORE_EMULATOR_HOST) is not None
2222
FIRESTORE_OTHER_DB = os.environ.get("SYSTEM_TESTS_DATABASE", "system-tests-named-db")

packages/google-cloud-firestore/tests/system/test_system.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,7 +1268,7 @@ def test_unicode_doc(client, cleanup, database):
12681268
assert snapshot2.reference.id == explicit_doc_id
12691269

12701270

1271-
@pytest.fixture
1271+
@pytest.fixture(scope="module")
12721272
def query_docs(client, database):
12731273
collection_id = "qs" + UNIQUE_RESOURCE_ID
12741274
sub_collection = "child" + UNIQUE_RESOURCE_ID
@@ -1297,13 +1297,13 @@ def query_docs(client, database):
12971297
operation()
12981298

12991299

1300-
@pytest.fixture
1300+
@pytest.fixture(scope="module")
13011301
def collection(query_docs):
13021302
collection, _, _ = query_docs
13031303
return collection
13041304

13051305

1306-
@pytest.fixture
1306+
@pytest.fixture(scope="module")
13071307
def query(collection):
13081308
return collection.where(filter=FieldFilter("a", "==", 1))
13091309

@@ -2336,7 +2336,11 @@ def test_watch_document(client, cleanup, database):
23362336
doc_ref.set({"first": "Jane", "last": "Doe", "born": 1900})
23372337
cleanup(doc_ref.delete)
23382338

2339-
sleep(1)
2339+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17428):
2340+
# Investigate why these sleep/polling delays are needed for listener tests.
2341+
# Having arbitrary delays is fragile and can lead to flakiness.
2342+
# Explore event-driven synchronization.
2343+
sleep(0.2)
23402344

23412345
# Setup listener
23422346
def on_snapshot(docs, changes, read_time):
@@ -2349,12 +2353,12 @@ def on_snapshot(docs, changes, read_time):
23492353
# Alter document
23502354
doc_ref.set({"first": "Ada", "last": "Lovelace", "born": 1815})
23512355

2352-
sleep(1)
2356+
sleep(0.2)
23532357

2354-
for _ in range(10):
2358+
for _ in range(50):
23552359
if on_snapshot.called_count > 0:
23562360
break
2357-
sleep(1)
2361+
sleep(0.2)
23582362

23592363
if on_snapshot.called_count not in (1, 2):
23602364
raise AssertionError(
@@ -2384,15 +2388,19 @@ def on_snapshot(docs, changes, read_time):
23842388

23852389
collection_ref.on_snapshot(on_snapshot)
23862390

2391+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17428):
2392+
# Investigate why these sleep/polling delays are needed for listener tests.
2393+
# Having arbitrary delays is fragile and can lead to flakiness.
2394+
# Explore event-driven synchronization.
23872395
# delay here so initial on_snapshot occurs and isn't combined with set
2388-
sleep(1)
2396+
sleep(0.2)
23892397

23902398
doc_ref.set({"first": "Ada", "last": "Lovelace", "born": 1815})
23912399

2392-
for _ in range(10):
2400+
for _ in range(50):
23932401
if on_snapshot.born == 1815:
23942402
break
2395-
sleep(1)
2403+
sleep(0.2)
23962404

23972405
if on_snapshot.born != 1815:
23982406
raise AssertionError(
@@ -2411,7 +2419,11 @@ def test_watch_query(client, cleanup, database):
24112419
doc_ref.set({"first": "Jane", "last": "Doe", "born": 1900})
24122420
cleanup(doc_ref.delete)
24132421

2414-
sleep(1)
2422+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17428):
2423+
# Investigate why these sleep/polling delays are needed for listener tests.
2424+
# Having arbitrary delays is fragile and can lead to flakiness.
2425+
# Explore event-driven synchronization.
2426+
sleep(0.2)
24152427

24162428
# Setup listener
24172429
def on_snapshot(docs, changes, read_time):
@@ -2429,10 +2441,10 @@ def on_snapshot(docs, changes, read_time):
24292441
# Alter document
24302442
doc_ref.set({"first": "Ada", "last": "Lovelace", "born": 1815})
24312443

2432-
for _ in range(10):
2444+
for _ in range(50):
24332445
if on_snapshot.called_count == 1:
24342446
return
2435-
sleep(1)
2447+
sleep(0.2)
24362448

24372449
if on_snapshot.called_count != 1:
24382450
raise AssertionError(
@@ -2806,7 +2818,11 @@ def on_snapshot(docs, changes, read_time):
28062818
on_snapshot.failed = None
28072819
query_ref.on_snapshot(on_snapshot)
28082820

2809-
sleep(1)
2821+
# TODO(https://github.com/googleapis/google-cloud-python/issues/17428):
2822+
# Investigate why these sleep/polling delays are needed for listener tests.
2823+
# Having arbitrary delays is fragile and can lead to flakiness.
2824+
# Explore event-driven synchronization.
2825+
sleep(0.2)
28102826

28112827
doc_ref1.set({"first": "Ada", "last": "Lovelace", "born": 1815})
28122828
cleanup(doc_ref1.delete)
@@ -2823,10 +2839,10 @@ def on_snapshot(docs, changes, read_time):
28232839
doc_ref5.set({"first": "Ada", "last": "lovelace", "born": 1815})
28242840
cleanup(doc_ref5.delete)
28252841

2826-
for _ in range(10):
2842+
for _ in range(50):
28272843
if on_snapshot.last_doc_count == 5:
28282844
break
2829-
sleep(1)
2845+
sleep(0.2)
28302846

28312847
if on_snapshot.failed:
28322848
raise on_snapshot.failed

packages/google-cloud-firestore/tests/system/test_system_async.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,7 +1243,7 @@ async def test_list_collections_with_read_time(client, cleanup, database):
12431243
}
12441244

12451245

1246-
@pytest_asyncio.fixture
1246+
@pytest_asyncio.fixture(scope="module")
12471247
async def query_docs(client):
12481248
collection_id = "qs" + UNIQUE_RESOURCE_ID
12491249
sub_collection = "child" + UNIQUE_RESOURCE_ID
@@ -1272,13 +1272,13 @@ async def query_docs(client):
12721272
await operation()
12731273

12741274

1275-
@pytest_asyncio.fixture
1275+
@pytest_asyncio.fixture(scope="module")
12761276
async def collection(query_docs):
12771277
collection, _, _ = query_docs
12781278
yield collection
12791279

12801280

1281-
@pytest_asyncio.fixture
1281+
@pytest_asyncio.fixture(scope="module")
12821282
async def async_query(collection):
12831283
return collection.where(filter=FieldFilter("a", "==", 1))
12841284

0 commit comments

Comments
 (0)