diff --git a/caldav/config.py b/caldav/config.py index d0008506..05c8af72 100644 --- a/caldav/config.py +++ b/caldav/config.py @@ -112,9 +112,13 @@ def read_config(fn, interactive_error=False): f"config file {fn} is not valid JSON, and pyyaml is not installed" ) from None - except FileNotFoundError: - ## File not found - logging.debug(f"config file {fn} not found") + except OSError as e: + ## Optional config file is unusable (missing, a directory, no + ## permission, ...). Treat any of these as "no config here" rather + ## than letting e.g. IsADirectoryError propagate out of an optional + ## config-file probe (happens when HOME is unset and the path expands + ## to something like '//.config//caldav/calendar.conf'). + logging.debug(f"config file {fn} not usable: {e}") return {} except ValueError: # Re-raise ValueError so caller can handle config errors diff --git a/caldav/testing.py b/caldav/testing.py index 28a552d8..1211f30b 100644 --- a/caldav/testing.py +++ b/caldav/testing.py @@ -213,18 +213,24 @@ def stop(self) -> None: if self.xapp_loop and self.xapp_runner: - async def cleanup_and_stop() -> None: + async def cleanup() -> None: await self.xapp_runner.cleanup() - self.xapp_loop.stop() try: - asyncio.run_coroutine_threadsafe(cleanup_and_stop(), self.xapp_loop).result( - timeout=10 - ) + asyncio.run_coroutine_threadsafe(cleanup(), self.xapp_loop).result(timeout=10) except Exception: - if self.xapp_loop: - self.xapp_loop.call_soon_threadsafe(self.xapp_loop.stop) - elif self.xapp_loop: + # Best-effort cleanup: we swallow anything it raises (timeout, + # CancelledError, aiohttp errors). The Xandikos server is + # ephemeral per test against a throwaway serverdir, so there is + # no shared state to release and nothing to recover here. + pass + + # Stop the loop from *outside* any coroutine running on it. Calling + # loop.stop() from within a coroutine scheduled via + # run_coroutine_threadsafe can stop the loop before it delivers that + # coroutine's result to the concurrent.futures.Future, so .result() + # would block until its timeout (~10s) on every single stop(). + if self.xapp_loop: self.xapp_loop.call_soon_threadsafe(self.xapp_loop.stop) if self.thread: diff --git a/tests/test_async_integration.py b/tests/test_async_integration.py index 97565781..ce1c10aa 100644 --- a/tests/test_async_integration.py +++ b/tests/test_async_integration.py @@ -186,9 +186,15 @@ def check_compatibility_flag(self, flag: str) -> bool: return flag in getattr(self._features, "_old_flags", []) @pytest.fixture(scope="class") - def test_server(self) -> TestServer: - """Get the test server for this class.""" - server = self.server + @classmethod + def test_server(cls) -> TestServer: + """Get the test server for this class. + + Defined as a classmethod because pytest deprecates class-scoped + fixtures written as plain instance methods (each test gets a fresh + instance, so per-instance state set here would not be visible anyway). + """ + server = cls.server server.start() yield server # Stop the server to free the port for other test modules