From 0ef63fad164e42fabed07aba0e50c94a484ffed9 Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Thu, 16 Apr 2026 07:49:45 +0530 Subject: [PATCH 1/3] Fix: raise soft open-file limit at serve() startup Python does not automatically raise the process soft open-file limit to the hard limit on startup, unlike the Go and Java runtimes. Under container environments that set a low soft limit (e.g. 1024), functions fail under load with "too many open files" errors. Add _raise_nofile_limit() in a new src/func_python/_ulimit.py module and call it at the top of serve() in both http.py and cloudevent.py. The helper caps the target at 65536 to avoid passing RLIM_INFINITY directly to setrlimit (which the kernel rejects), and silently skips on non-Unix platforms where the resource module is unavailable. Add six unit tests in tests/test_ulimit.py covering normal raise, no-op when soft equals hard, RLIM_INFINITY capping, ImportError, OSError, and ValueError cases. Relates to knative/func#3513 --- src/func_python/_ulimit.py | 30 ++++++++++ src/func_python/cloudevent.py | 2 + src/func_python/http.py | 2 + tests/test_ulimit.py | 106 ++++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 src/func_python/_ulimit.py create mode 100644 tests/test_ulimit.py diff --git a/src/func_python/_ulimit.py b/src/func_python/_ulimit.py new file mode 100644 index 00000000..720efb5e --- /dev/null +++ b/src/func_python/_ulimit.py @@ -0,0 +1,30 @@ +import logging + +_MAX_NOFILE = 65536 # safe cap when hard == RLIM_INFINITY + + +def _raise_nofile_limit(): + """Raise the process soft open-file limit to the hard limit. + + Matches the automatic behaviour of the Go and Java runtimes. + Silently skips on non-Unix platforms where resource is unavailable. + """ + try: + import resource + except ImportError: + return # non-Unix (e.g. Windows) — skip + + try: + soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + if soft < hard: + # RLIM_INFINITY (9223372036854775807 on Linux) cannot be passed + # directly to setrlimit — the kernel rejects it with OSError. + # Cap the target at a known-safe value instead. + if hard == resource.RLIM_INFINITY: + target = _MAX_NOFILE + else: + target = min(hard, _MAX_NOFILE) + resource.setrlimit(resource.RLIMIT_NOFILE, (target, hard)) + logging.info("Raised open-file limit from %d to %d", soft, target) + except (ValueError, OSError) as e: + logging.warning("Could not raise open-file limit: %s", e) diff --git a/src/func_python/cloudevent.py b/src/func_python/cloudevent.py index 8465803a..101742f4 100644 --- a/src/func_python/cloudevent.py +++ b/src/func_python/cloudevent.py @@ -13,6 +13,7 @@ from cloudevents.core.exceptions import CloudEventValidationError import func_python.sock +from func_python._ulimit import _raise_nofile_limit DEFAULT_LOG_LEVEL = logging.INFO @@ -24,6 +25,7 @@ def serve(f): and starting. The function can be either a constructor for a functon instance (named "new") or a simple ASGI handler function (named "handle"). """ + _raise_nofile_limit() logging.debug("func runtime creating function instance") if f.__name__ == 'new': diff --git a/src/func_python/http.py b/src/func_python/http.py index 4133f51d..440a692d 100644 --- a/src/func_python/http.py +++ b/src/func_python/http.py @@ -8,6 +8,7 @@ import hypercorn.asyncio import func_python.sock +from func_python._ulimit import _raise_nofile_limit DEFAULT_LOG_LEVEL = logging.INFO @@ -19,6 +20,7 @@ def serve(f): and starting. The function can be either a constructor for a functon instance (named "new") or a simple ASGI handler function (named "handle"). """ + _raise_nofile_limit() logging.debug("func runtime creating function instance") if f.__name__ == 'new': diff --git a/tests/test_ulimit.py b/tests/test_ulimit.py new file mode 100644 index 00000000..b0fa26d3 --- /dev/null +++ b/tests/test_ulimit.py @@ -0,0 +1,106 @@ +import importlib +import logging +import sys +import unittest +from unittest.mock import MagicMock, patch, call + + +def _load_ulimit(mock_resource=None): + """Import (or reload) _ulimit with an optional resource module mock.""" + if mock_resource is not None: + with patch.dict(sys.modules, {"resource": mock_resource}): + import func_python._ulimit as ulimit + importlib.reload(ulimit) + else: + import func_python._ulimit as ulimit + importlib.reload(ulimit) + return ulimit + + +class TestRaiseNofileLimit(unittest.TestCase): + + def _make_resource(self, soft, hard, rlim_infinity=None): + """Build a minimal mock of the resource module.""" + mock_resource = MagicMock() + mock_resource.RLIMIT_NOFILE = 7 # any constant + mock_resource.getrlimit.return_value = (soft, hard) + mock_resource.RLIM_INFINITY = ( + rlim_infinity if rlim_infinity is not None else 9223372036854775807 + ) + return mock_resource + + def test_raises_soft_limit_to_hard(self): + """When soft < hard, setrlimit is called with the target value.""" + mock_resource = self._make_resource(soft=1024, hard=4096) + ulimit = _load_ulimit(mock_resource) + + with patch.dict(sys.modules, {"resource": mock_resource}): + ulimit._raise_nofile_limit() + + mock_resource.setrlimit.assert_called_once_with( + mock_resource.RLIMIT_NOFILE, (4096, 4096) + ) + + def test_no_change_when_soft_equals_hard(self): + """When soft == hard, setrlimit must not be called.""" + mock_resource = self._make_resource(soft=4096, hard=4096) + ulimit = _load_ulimit(mock_resource) + + with patch.dict(sys.modules, {"resource": mock_resource}): + ulimit._raise_nofile_limit() + + mock_resource.setrlimit.assert_not_called() + + def test_rlim_infinity_capped_at_max(self): + """When hard == RLIM_INFINITY the target must be capped at _MAX_NOFILE.""" + RLIM_INFINITY = 9223372036854775807 + mock_resource = self._make_resource(soft=1024, hard=RLIM_INFINITY, + rlim_infinity=RLIM_INFINITY) + ulimit = _load_ulimit(mock_resource) + + with patch.dict(sys.modules, {"resource": mock_resource}): + ulimit._raise_nofile_limit() + + mock_resource.setrlimit.assert_called_once_with( + mock_resource.RLIMIT_NOFILE, (ulimit._MAX_NOFILE, RLIM_INFINITY) + ) + + def test_import_error_is_silently_skipped(self): + """When resource is unavailable (non-Unix), no exception is raised.""" + with patch.dict(sys.modules, {"resource": None}): + import func_python._ulimit as ulimit + importlib.reload(ulimit) + # Should complete without raising anything + ulimit._raise_nofile_limit() + + def test_os_error_logs_warning(self, caplog=None): + """When setrlimit raises OSError, a warning is logged and no exception propagates.""" + mock_resource = self._make_resource(soft=1024, hard=4096) + mock_resource.setrlimit.side_effect = OSError("operation not permitted") + ulimit = _load_ulimit(mock_resource) + + with patch.dict(sys.modules, {"resource": mock_resource}): + with self.assertLogs(level=logging.WARNING) as log_ctx: + ulimit._raise_nofile_limit() + + self.assertTrue( + any("Could not raise open-file limit" in msg for msg in log_ctx.output) + ) + + def test_value_error_logs_warning(self): + """When setrlimit raises ValueError, a warning is logged and no exception propagates.""" + mock_resource = self._make_resource(soft=1024, hard=4096) + mock_resource.setrlimit.side_effect = ValueError("invalid argument") + ulimit = _load_ulimit(mock_resource) + + with patch.dict(sys.modules, {"resource": mock_resource}): + with self.assertLogs(level=logging.WARNING) as log_ctx: + ulimit._raise_nofile_limit() + + self.assertTrue( + any("Could not raise open-file limit" in msg for msg in log_ctx.output) + ) + + +if __name__ == "__main__": + unittest.main() From d878ddeb356a5ec3d8c1cebfe2b714cdf3617f0d Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Thu, 16 Apr 2026 13:28:30 +0530 Subject: [PATCH 2/3] fix: correctness and test-quality issues from code review - Remove min(hard, _MAX_NOFILE) cap for finite hard limits; raise soft to the actual hard limit as the Go/Java runtimes do. The _MAX_NOFILE cap now applies only to the RLIM_INFINITY branch where it is needed. - Fix misleading comment: it is the soft limit, not the hard limit, that cannot be RLIM_INFINITY without CAP_SYS_RESOURCE. - Replace importlib.reload()-based test helpers with plain pytest functions that patch sys.modules only at call time, eliminating shared module-state mutation between tests. - Remove dead caplog=None default parameter from test_os_error_logs_warning; caplog is now injected as a proper pytest fixture. - Add test_raises_soft_limit_to_hard_above_65536 to guard against the truncation regression. - Add wire-up tests for http.serve() and cloudevent.serve() confirming _raise_nofile_limit() is called on every serve() invocation. --- src/func_python/_ulimit.py | 10 +- tests/test_ulimit.py | 224 ++++++++++++++++++++----------------- 2 files changed, 126 insertions(+), 108 deletions(-) diff --git a/src/func_python/_ulimit.py b/src/func_python/_ulimit.py index 720efb5e..8c0c993b 100644 --- a/src/func_python/_ulimit.py +++ b/src/func_python/_ulimit.py @@ -17,13 +17,15 @@ def _raise_nofile_limit(): try: soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) if soft < hard: - # RLIM_INFINITY (9223372036854775807 on Linux) cannot be passed - # directly to setrlimit — the kernel rejects it with OSError. - # Cap the target at a known-safe value instead. + # When hard is RLIM_INFINITY the kernel rejects setting the soft + # limit to RLIM_INFINITY without CAP_SYS_RESOURCE, so cap the + # soft limit at a known-safe value. For finite hard limits, raise + # the soft limit all the way to the hard limit as the Go and Java + # runtimes do. if hard == resource.RLIM_INFINITY: target = _MAX_NOFILE else: - target = min(hard, _MAX_NOFILE) + target = hard resource.setrlimit(resource.RLIMIT_NOFILE, (target, hard)) logging.info("Raised open-file limit from %d to %d", soft, target) except (ValueError, OSError) as e: diff --git a/tests/test_ulimit.py b/tests/test_ulimit.py index b0fa26d3..7fa680d6 100644 --- a/tests/test_ulimit.py +++ b/tests/test_ulimit.py @@ -1,106 +1,122 @@ -import importlib import logging import sys -import unittest -from unittest.mock import MagicMock, patch, call - - -def _load_ulimit(mock_resource=None): - """Import (or reload) _ulimit with an optional resource module mock.""" - if mock_resource is not None: - with patch.dict(sys.modules, {"resource": mock_resource}): - import func_python._ulimit as ulimit - importlib.reload(ulimit) - else: - import func_python._ulimit as ulimit - importlib.reload(ulimit) - return ulimit - - -class TestRaiseNofileLimit(unittest.TestCase): - - def _make_resource(self, soft, hard, rlim_infinity=None): - """Build a minimal mock of the resource module.""" - mock_resource = MagicMock() - mock_resource.RLIMIT_NOFILE = 7 # any constant - mock_resource.getrlimit.return_value = (soft, hard) - mock_resource.RLIM_INFINITY = ( - rlim_infinity if rlim_infinity is not None else 9223372036854775807 - ) - return mock_resource - - def test_raises_soft_limit_to_hard(self): - """When soft < hard, setrlimit is called with the target value.""" - mock_resource = self._make_resource(soft=1024, hard=4096) - ulimit = _load_ulimit(mock_resource) - - with patch.dict(sys.modules, {"resource": mock_resource}): - ulimit._raise_nofile_limit() - - mock_resource.setrlimit.assert_called_once_with( - mock_resource.RLIMIT_NOFILE, (4096, 4096) - ) - - def test_no_change_when_soft_equals_hard(self): - """When soft == hard, setrlimit must not be called.""" - mock_resource = self._make_resource(soft=4096, hard=4096) - ulimit = _load_ulimit(mock_resource) - - with patch.dict(sys.modules, {"resource": mock_resource}): - ulimit._raise_nofile_limit() - - mock_resource.setrlimit.assert_not_called() - - def test_rlim_infinity_capped_at_max(self): - """When hard == RLIM_INFINITY the target must be capped at _MAX_NOFILE.""" - RLIM_INFINITY = 9223372036854775807 - mock_resource = self._make_resource(soft=1024, hard=RLIM_INFINITY, - rlim_infinity=RLIM_INFINITY) - ulimit = _load_ulimit(mock_resource) - - with patch.dict(sys.modules, {"resource": mock_resource}): - ulimit._raise_nofile_limit() - - mock_resource.setrlimit.assert_called_once_with( - mock_resource.RLIMIT_NOFILE, (ulimit._MAX_NOFILE, RLIM_INFINITY) - ) - - def test_import_error_is_silently_skipped(self): - """When resource is unavailable (non-Unix), no exception is raised.""" - with patch.dict(sys.modules, {"resource": None}): - import func_python._ulimit as ulimit - importlib.reload(ulimit) - # Should complete without raising anything - ulimit._raise_nofile_limit() - - def test_os_error_logs_warning(self, caplog=None): - """When setrlimit raises OSError, a warning is logged and no exception propagates.""" - mock_resource = self._make_resource(soft=1024, hard=4096) - mock_resource.setrlimit.side_effect = OSError("operation not permitted") - ulimit = _load_ulimit(mock_resource) - - with patch.dict(sys.modules, {"resource": mock_resource}): - with self.assertLogs(level=logging.WARNING) as log_ctx: - ulimit._raise_nofile_limit() - - self.assertTrue( - any("Could not raise open-file limit" in msg for msg in log_ctx.output) - ) - - def test_value_error_logs_warning(self): - """When setrlimit raises ValueError, a warning is logged and no exception propagates.""" - mock_resource = self._make_resource(soft=1024, hard=4096) - mock_resource.setrlimit.side_effect = ValueError("invalid argument") - ulimit = _load_ulimit(mock_resource) - - with patch.dict(sys.modules, {"resource": mock_resource}): - with self.assertLogs(level=logging.WARNING) as log_ctx: - ulimit._raise_nofile_limit() - - self.assertTrue( - any("Could not raise open-file limit" in msg for msg in log_ctx.output) - ) - - -if __name__ == "__main__": - unittest.main() +from unittest.mock import MagicMock, patch + + +_RLIM_INFINITY = 9223372036854775807 + + +def _make_resource(soft, hard, rlim_infinity=_RLIM_INFINITY): + """Build a minimal mock of the resource module.""" + mock = MagicMock() + mock.RLIMIT_NOFILE = 7 + mock.RLIM_INFINITY = rlim_infinity + mock.getrlimit.return_value = (soft, hard) + return mock + + +def _call_with_resource(mock_resource): + """Call _raise_nofile_limit() with the given resource mock in place.""" + with patch.dict(sys.modules, {"resource": mock_resource}): + from func_python._ulimit import _raise_nofile_limit + _raise_nofile_limit() + return mock_resource + + +# --------------------------------------------------------------------------- +# Unit tests for _raise_nofile_limit() +# --------------------------------------------------------------------------- + +def test_raises_soft_limit_to_hard(): + """When soft < hard (finite), setrlimit is called with the full hard value.""" + mock_resource = _make_resource(soft=1024, hard=4096) + _call_with_resource(mock_resource) + mock_resource.setrlimit.assert_called_once_with( + mock_resource.RLIMIT_NOFILE, (4096, 4096) + ) + + +def test_raises_soft_limit_to_hard_above_65536(): + """Hard limits above 65536 must be honoured in full, not capped.""" + mock_resource = _make_resource(soft=1024, hard=131072) + _call_with_resource(mock_resource) + mock_resource.setrlimit.assert_called_once_with( + mock_resource.RLIMIT_NOFILE, (131072, 131072) + ) + + +def test_no_change_when_soft_equals_hard(): + """When soft == hard, setrlimit must not be called.""" + mock_resource = _make_resource(soft=4096, hard=4096) + _call_with_resource(mock_resource) + mock_resource.setrlimit.assert_not_called() + + +def test_rlim_infinity_capped_at_max(): + """When hard == RLIM_INFINITY the soft limit must be capped at _MAX_NOFILE.""" + from func_python._ulimit import _MAX_NOFILE + mock_resource = _make_resource(soft=1024, hard=_RLIM_INFINITY, + rlim_infinity=_RLIM_INFINITY) + _call_with_resource(mock_resource) + mock_resource.setrlimit.assert_called_once_with( + mock_resource.RLIMIT_NOFILE, (_MAX_NOFILE, _RLIM_INFINITY) + ) + + +def test_import_error_is_silently_skipped(): + """When resource is unavailable (non-Unix), no exception is raised.""" + with patch.dict(sys.modules, {"resource": None}): + from func_python._ulimit import _raise_nofile_limit + _raise_nofile_limit() # must not raise + + +def test_os_error_logs_warning(caplog): + """When setrlimit raises OSError, a warning is logged and no exception propagates.""" + mock_resource = _make_resource(soft=1024, hard=4096) + mock_resource.setrlimit.side_effect = OSError("operation not permitted") + with caplog.at_level(logging.WARNING): + _call_with_resource(mock_resource) + assert any("Could not raise open-file limit" in r.message for r in caplog.records) + + +def test_value_error_logs_warning(caplog): + """When setrlimit raises ValueError, a warning is logged and no exception propagates.""" + mock_resource = _make_resource(soft=1024, hard=4096) + mock_resource.setrlimit.side_effect = ValueError("invalid argument") + with caplog.at_level(logging.WARNING): + _call_with_resource(mock_resource) + assert any("Could not raise open-file limit" in r.message for r in caplog.records) + + +# --------------------------------------------------------------------------- +# Wire-up tests: verify serve() in http.py and cloudevent.py call the helper +# --------------------------------------------------------------------------- + +def test_http_serve_calls_raise_nofile_limit(): + """serve() in http.py must call _raise_nofile_limit() before doing anything else.""" + with patch("func_python.http._raise_nofile_limit") as mock_fn: + with patch("func_python.http.ASGIApplication") as mock_app: + mock_app.return_value.serve.return_value = None + from func_python.http import serve + + async def handle(scope, receive, send): + pass + + serve(handle) + + mock_fn.assert_called_once() + + +def test_cloudevent_serve_calls_raise_nofile_limit(): + """serve() in cloudevent.py must call _raise_nofile_limit() before doing anything else.""" + with patch("func_python.cloudevent._raise_nofile_limit") as mock_fn: + with patch("func_python.cloudevent.ASGIApplication") as mock_app: + mock_app.return_value.serve.return_value = None + from func_python.cloudevent import serve + + async def handle(scope, receive, send): + pass + + serve(handle) + + mock_fn.assert_called_once() From 1d3975400ae7bab39ee96da9dad8b35c6232e6f7 Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Thu, 16 Apr 2026 21:25:19 +0530 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20address=20strict=20review=20?= =?UTF-8?q?=E2=80=94=20mock=20isolation,=20log=20level,=20named=20logger,?= =?UTF-8?q?=20versioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move `import resource` to module level as `_resource` so tests can patch `func_python._ulimit._resource` directly with unittest.mock.patch, replacing fragile sys.modules manipulation that lacked guaranteed isolation. - Replace `logging.info` with `_logger.debug` (named logger via getLogger(__name__)): routine startup detail should not appear at INFO in production logs; named logger lets users silence func_python logs independently. - Use `resource.RLIM_INFINITY` from the real module in tests instead of a hardcoded magic integer that could silently diverge on non-Linux platforms. - Bump version to 0.8.2 and add CHANGELOG entry for the ulimit feature. --- CHANGELOG.md | 10 ++++++++++ pyproject.toml | 2 +- src/func_python/_ulimit.py | 27 ++++++++++++++++----------- tests/test_ulimit.py | 34 ++++++++++++++++------------------ 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3061f1b9..8fc4ef3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixed ### Security +## 0.8.2 - 2026-04-16 + +### Added + +- Automatically raise the soft `RLIMIT_NOFILE` to the hard limit at + `serve()` startup, matching Go and Java runtime behaviour. Finite hard + limits are honoured in full; when the hard limit is `RLIM_INFINITY` the + soft limit is capped at 65536 to stay within kernel constraints. + Failures are logged as warnings and never fatal (knative/func#3513). + ## 0.8.1 - 2026-04-14 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 4be72430..d2f4585d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "func-python" -version = "0.8.1" +version = "0.8.2" description = "Knative Functions Python Middleware" authors = ["The Knative Authors "] readme = "README.md" diff --git a/src/func_python/_ulimit.py b/src/func_python/_ulimit.py index 8c0c993b..c05d8401 100644 --- a/src/func_python/_ulimit.py +++ b/src/func_python/_ulimit.py @@ -1,7 +1,17 @@ import logging +# Module-level import so tests can patch func_python._ulimit._resource directly. +# On non-Unix platforms (e.g. Windows) the resource module is unavailable; +# _resource is set to None and _raise_nofile_limit() becomes a no-op. +try: + import resource as _resource +except ImportError: + _resource = None + _MAX_NOFILE = 65536 # safe cap when hard == RLIM_INFINITY +_logger = logging.getLogger(__name__) + def _raise_nofile_limit(): """Raise the process soft open-file limit to the hard limit. @@ -9,24 +19,19 @@ def _raise_nofile_limit(): Matches the automatic behaviour of the Go and Java runtimes. Silently skips on non-Unix platforms where resource is unavailable. """ - try: - import resource - except ImportError: + if _resource is None: return # non-Unix (e.g. Windows) — skip try: - soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + soft, hard = _resource.getrlimit(_resource.RLIMIT_NOFILE) if soft < hard: # When hard is RLIM_INFINITY the kernel rejects setting the soft # limit to RLIM_INFINITY without CAP_SYS_RESOURCE, so cap the # soft limit at a known-safe value. For finite hard limits, raise # the soft limit all the way to the hard limit as the Go and Java # runtimes do. - if hard == resource.RLIM_INFINITY: - target = _MAX_NOFILE - else: - target = hard - resource.setrlimit(resource.RLIMIT_NOFILE, (target, hard)) - logging.info("Raised open-file limit from %d to %d", soft, target) + target = _MAX_NOFILE if hard == _resource.RLIM_INFINITY else hard + _resource.setrlimit(_resource.RLIMIT_NOFILE, (target, hard)) + _logger.debug("Raised open-file limit from %d to %d", soft, target) except (ValueError, OSError) as e: - logging.warning("Could not raise open-file limit: %s", e) + _logger.warning("Could not raise open-file limit: %s", e) diff --git a/tests/test_ulimit.py b/tests/test_ulimit.py index 7fa680d6..f7a6b1bd 100644 --- a/tests/test_ulimit.py +++ b/tests/test_ulimit.py @@ -1,23 +1,20 @@ import logging -import sys +import resource from unittest.mock import MagicMock, patch -_RLIM_INFINITY = 9223372036854775807 - - -def _make_resource(soft, hard, rlim_infinity=_RLIM_INFINITY): +def _make_resource(soft, hard, rlim_infinity=None): """Build a minimal mock of the resource module.""" - mock = MagicMock() - mock.RLIMIT_NOFILE = 7 - mock.RLIM_INFINITY = rlim_infinity + mock = MagicMock(spec=resource) + mock.RLIMIT_NOFILE = resource.RLIMIT_NOFILE + mock.RLIM_INFINITY = rlim_infinity if rlim_infinity is not None else resource.RLIM_INFINITY mock.getrlimit.return_value = (soft, hard) return mock def _call_with_resource(mock_resource): - """Call _raise_nofile_limit() with the given resource mock in place.""" - with patch.dict(sys.modules, {"resource": mock_resource}): + """Call _raise_nofile_limit() with the given resource mock patched in place.""" + with patch("func_python._ulimit._resource", mock_resource): from func_python._ulimit import _raise_nofile_limit _raise_nofile_limit() return mock_resource @@ -32,7 +29,7 @@ def test_raises_soft_limit_to_hard(): mock_resource = _make_resource(soft=1024, hard=4096) _call_with_resource(mock_resource) mock_resource.setrlimit.assert_called_once_with( - mock_resource.RLIMIT_NOFILE, (4096, 4096) + resource.RLIMIT_NOFILE, (4096, 4096) ) @@ -41,7 +38,7 @@ def test_raises_soft_limit_to_hard_above_65536(): mock_resource = _make_resource(soft=1024, hard=131072) _call_with_resource(mock_resource) mock_resource.setrlimit.assert_called_once_with( - mock_resource.RLIMIT_NOFILE, (131072, 131072) + resource.RLIMIT_NOFILE, (131072, 131072) ) @@ -55,17 +52,18 @@ def test_no_change_when_soft_equals_hard(): def test_rlim_infinity_capped_at_max(): """When hard == RLIM_INFINITY the soft limit must be capped at _MAX_NOFILE.""" from func_python._ulimit import _MAX_NOFILE - mock_resource = _make_resource(soft=1024, hard=_RLIM_INFINITY, - rlim_infinity=_RLIM_INFINITY) + rlim_infinity = resource.RLIM_INFINITY + mock_resource = _make_resource(soft=1024, hard=rlim_infinity, + rlim_infinity=rlim_infinity) _call_with_resource(mock_resource) mock_resource.setrlimit.assert_called_once_with( - mock_resource.RLIMIT_NOFILE, (_MAX_NOFILE, _RLIM_INFINITY) + resource.RLIMIT_NOFILE, (_MAX_NOFILE, rlim_infinity) ) def test_import_error_is_silently_skipped(): """When resource is unavailable (non-Unix), no exception is raised.""" - with patch.dict(sys.modules, {"resource": None}): + with patch("func_python._ulimit._resource", None): from func_python._ulimit import _raise_nofile_limit _raise_nofile_limit() # must not raise @@ -74,7 +72,7 @@ def test_os_error_logs_warning(caplog): """When setrlimit raises OSError, a warning is logged and no exception propagates.""" mock_resource = _make_resource(soft=1024, hard=4096) mock_resource.setrlimit.side_effect = OSError("operation not permitted") - with caplog.at_level(logging.WARNING): + with caplog.at_level(logging.WARNING, logger="func_python._ulimit"): _call_with_resource(mock_resource) assert any("Could not raise open-file limit" in r.message for r in caplog.records) @@ -83,7 +81,7 @@ def test_value_error_logs_warning(caplog): """When setrlimit raises ValueError, a warning is logged and no exception propagates.""" mock_resource = _make_resource(soft=1024, hard=4096) mock_resource.setrlimit.side_effect = ValueError("invalid argument") - with caplog.at_level(logging.WARNING): + with caplog.at_level(logging.WARNING, logger="func_python._ulimit"): _call_with_resource(mock_resource) assert any("Could not raise open-file limit" in r.message for r in caplog.records)