Skip to content

Commit d09ec45

Browse files
committed
fix: prevent 513 setup errors and async/sync UID conflicts in CI
Three root causes addressed: 1. verify_docker() in commit e643035 was broadened to return True when the `docker compose` plugin is available, even when the standalone `docker-compose` binary is absent. But start.sh scripts require the standalone binary, so on GitHub CI: docker_available=True → all docker server directories registered → setup called → docker-compose: command not found (exit 127) → 513 test setup ERRORs. Fix: verify_docker() checks only the standalone `docker-compose` binary. On GitHub CI it now returns False, so only service containers that are already accessible are registered (via the is_accessible() branch of the existing `docker_available or server.is_accessible()` check). Locally, where docker-compose is installed, auto-start still works. 2. async_task_list used stable cal_id "pythoncaldav-async-test" for all mixed-calendar servers, including Cyrus which has save.duplicate-uid.cross-calendar=ungraceful. Async test created UID well_known_1 in pythoncaldav-async-test; sync testObjectByUID tried the same UID in pythoncaldav-test-tasks → Cyrus 403. This conflict was latent in 2a2d0ca but only surfaces once async tests actually reach Cyrus (which this branch enables). Fix: use "pythoncaldav-test-tasks" (same as sync suite) for any server with cross-calendar UID uniqueness enforcement. 3. Nextcloud's calendar deletion goes to trashbin (delete-calendar.free- namespace=fragile), so the next fixture invocation would find the old calendar, cleanup_calendar_objects would silently fail, and leftover objects triggered duplicate-UID 500 errors. Fix: use unique timestamped cal_id for servers where delete-calendar.free-namespace is not supported. prompt: the github run fails (investigate and fix CI failures on branch async-github-testruns) followup-prompt: continue implementing the three fixes identified in the previous session followup-prompt: it is intended that it should be possible to run pytest without having to manually start all the test servers followup-prompt: regarding the async/sync uid conflict, why don't we have that in the main branch?
1 parent 60f5622 commit d09ec45

2 files changed

Lines changed: 44 additions & 26 deletions

File tree

tests/test_async_integration.py

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -260,20 +260,47 @@ async def async_task_list(self, async_client: Any) -> Any:
260260
For servers that don't support mixed calendars (like Zimbra), todos must
261261
be stored in a separate task list with supported_calendar_component_set=["VTODO"].
262262
263-
Uses the same stable cal_id ("pythoncaldav-test-tasks") as the sync test suite
264-
so that both share state rather than accumulate duplicate-UID conflicts on
265-
servers with cross-calendar UID uniqueness (e.g. OX). Objects are wiped
266-
before each test for isolation.
263+
Calendar naming strategy:
264+
- Servers with cross-calendar UID uniqueness (Cyrus, OX) or no mixed-calendar
265+
support: use "pythoncaldav-test-tasks" (shared with sync suite) to avoid
266+
duplicate-UID conflicts.
267+
- Servers where calendar deletion doesn't free the namespace (Nextcloud trashbin):
268+
use unique timestamped names to avoid stale state from previous runs.
269+
- All other servers: use stable "pythoncaldav-async-test".
267270
"""
268271
from caldav.aio import AsyncPrincipal
269272
from caldav.lib.error import AuthorizationError, NotFoundError
270273

271274
from .fixture_helpers import aget_or_create_test_calendar, cleanup_calendar_objects
272275

273-
# Check if server supports mixed calendars
274-
supports_mixed = True
275-
if hasattr(async_client, "features") and async_client.features:
276-
supports_mixed = async_client.features.is_supported("save-load.todo.mixed-calendar")
276+
feats = getattr(async_client, "features", None) or None
277+
278+
def _feat(name: str) -> bool:
279+
return feats.is_supported(name) if feats else True
280+
281+
supports_mixed = _feat("save-load.todo.mixed-calendar")
282+
cross_cal_uid_issues = not _feat("save.duplicate-uid.cross-calendar")
283+
delete_frees_namespace = _feat("delete-calendar.free-namespace")
284+
285+
# Determine cal_id and whether we share state with the sync test suite
286+
if not supports_mixed or cross_cal_uid_issues:
287+
# Must share with sync suite to avoid cross-calendar UID conflicts
288+
component_set: list[str] | None = ["VTODO"] if not supports_mixed else None
289+
cal_id = "pythoncaldav-test-tasks"
290+
shared_with_sync = True
291+
elif not delete_frees_namespace:
292+
# Deletion goes to trashbin (e.g. Nextcloud): use unique name so
293+
# stale objects from a previous run don't cause duplicate-UID errors.
294+
component_set = None
295+
cal_id = f"pythoncaldav-async-test-{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
296+
shared_with_sync = False
297+
else:
298+
component_set = None
299+
cal_id = "pythoncaldav-async-test"
300+
shared_with_sync = False
301+
302+
supports_displayname = _feat("create-calendar.set-displayname")
303+
calendar_name = cal_id if supports_displayname else None
277304

278305
# Try to get principal for calendar operations
279306
principal = None
@@ -282,19 +309,6 @@ async def async_task_list(self, async_client: Any) -> Any:
282309
except (NotFoundError, AuthorizationError):
283310
pass
284311

285-
# For servers without mixed calendar support, create a dedicated task list.
286-
# Use the same stable cal_id as the sync test suite so servers with
287-
# cross-calendar duplicate-UID detection (e.g. OX) don't reject objects
288-
# that also exist in the sync test's calendar.
289-
component_set = ["VTODO"] if not supports_mixed else None
290-
cal_id = "pythoncaldav-test-tasks" if not supports_mixed else "pythoncaldav-async-test"
291-
supports_displayname = (
292-
async_client.features.is_supported("create-calendar.set-displayname")
293-
if hasattr(async_client, "features") and async_client.features
294-
else True
295-
)
296-
calendar_name = cal_id if supports_displayname else None
297-
298312
calendar, created = await aget_or_create_test_calendar(
299313
async_client,
300314
principal,
@@ -310,8 +324,10 @@ async def async_task_list(self, async_client: Any) -> Any:
310324

311325
yield calendar
312326

313-
# Only cleanup if we created the calendar
314-
if created:
327+
# Delete only if we created it and it's not shared with the sync suite.
328+
# For shared calendars, objects were already wiped at setup; deleting the
329+
# calendar here would break sync tests that run later in the same session.
330+
if created and not shared_with_sync:
315331
try:
316332
await calendar.delete()
317333
except Exception:

tests/test_servers/base.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,11 @@ def _run(*cmd: str) -> bool:
319319
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
320320
return False
321321

322-
# Accept either the legacy standalone binary or the modern plugin form.
323-
compose_ok = _run("docker-compose", "--version") or _run("docker", "compose", "version")
324-
return compose_ok and _run("docker", "ps")
322+
# start.sh scripts use the standalone `docker-compose` binary, so we
323+
# only return True when that binary is actually present. The `docker
324+
# compose` plugin form is NOT sufficient — start.sh will exit 127 if
325+
# only the plugin is available (e.g. on GitHub Actions runners).
326+
return _run("docker-compose", "--version") and _run("docker", "ps")
325327

326328
def start(self) -> None:
327329
"""

0 commit comments

Comments
 (0)