@@ -106,6 +106,102 @@ def setrlimit(_key: int, _val: tuple[int, int]) -> None:
106106 assert tree is not None
107107
108108
109+ def test_parse_limits_never_lowers_hard_limit (monkeypatch : pytest .MonkeyPatch ) -> None :
110+ calls : list [tuple [int , int ]] = []
111+
112+ class _DummyResource :
113+ RLIMIT_CPU = 0
114+ RLIM_INFINITY = 10 ** 9
115+
116+ @staticmethod
117+ def getrlimit (_key : int ) -> tuple [int , int ]:
118+ return (_DummyResource .RLIM_INFINITY , _DummyResource .RLIM_INFINITY )
119+
120+ @staticmethod
121+ def setrlimit (_key : int , val : tuple [int , int ]) -> None :
122+ calls .append (val )
123+ # Simulate a system where changing hard limit would fail.
124+ assert val [1 ] == _DummyResource .RLIM_INFINITY
125+
126+ monkeypatch .setattr (os , "name" , "posix" )
127+ monkeypatch .setattr (signal , "getsignal" , lambda * _args , ** _kwargs : None )
128+ monkeypatch .setattr (signal , "signal" , lambda * _args , ** _kwargs : None )
129+ monkeypatch .setattr (signal , "setitimer" , lambda * _args , ** _kwargs : None )
130+ monkeypatch .setitem (sys .modules , "resource" , _DummyResource )
131+
132+ with extractor ._parse_limits (5 ):
133+ pass
134+
135+ assert calls
136+ # First set lowers only soft limit, hard stays unchanged.
137+ assert calls [0 ] == (5 , _DummyResource .RLIM_INFINITY )
138+ # Final restore returns to original limits.
139+ assert calls [- 1 ] == (
140+ _DummyResource .RLIM_INFINITY ,
141+ _DummyResource .RLIM_INFINITY ,
142+ )
143+
144+
145+ def test_parse_limits_uses_finite_soft_limit_branch (
146+ monkeypatch : pytest .MonkeyPatch ,
147+ ) -> None :
148+ calls : list [tuple [int , int ]] = []
149+
150+ class _DummyResource :
151+ RLIMIT_CPU = 0
152+ RLIM_INFINITY = 10 ** 9
153+
154+ @staticmethod
155+ def getrlimit (_key : int ) -> tuple [int , int ]:
156+ return (20 , 20 )
157+
158+ @staticmethod
159+ def setrlimit (_key : int , val : tuple [int , int ]) -> None :
160+ calls .append (val )
161+
162+ monkeypatch .setattr (os , "name" , "posix" )
163+ monkeypatch .setattr (signal , "getsignal" , lambda * _args , ** _kwargs : None )
164+ monkeypatch .setattr (signal , "signal" , lambda * _args , ** _kwargs : None )
165+ monkeypatch .setattr (signal , "setitimer" , lambda * _args , ** _kwargs : None )
166+ monkeypatch .setitem (sys .modules , "resource" , _DummyResource )
167+
168+ with extractor ._parse_limits (5 ):
169+ pass
170+
171+ # New soft is min(timeout, old_soft, hard_ceiling), hard is preserved.
172+ assert calls [0 ] == (5 , 20 )
173+ assert calls [- 1 ] == (20 , 20 )
174+
175+
176+ def test_parse_limits_restore_failure_is_ignored (
177+ monkeypatch : pytest .MonkeyPatch ,
178+ ) -> None :
179+ class _DummyResource :
180+ RLIMIT_CPU = 0
181+ RLIM_INFINITY = 10 ** 9
182+ _calls = 0
183+
184+ @staticmethod
185+ def getrlimit (_key : int ) -> tuple [int , int ]:
186+ return (_DummyResource .RLIM_INFINITY , _DummyResource .RLIM_INFINITY )
187+
188+ @staticmethod
189+ def setrlimit (_key : int , _val : tuple [int , int ]) -> None :
190+ _DummyResource ._calls += 1
191+ if _DummyResource ._calls >= 2 :
192+ raise RuntimeError ("restore denied" )
193+
194+ monkeypatch .setattr (os , "name" , "posix" )
195+ monkeypatch .setattr (signal , "getsignal" , lambda * _args , ** _kwargs : None )
196+ monkeypatch .setattr (signal , "signal" , lambda * _args , ** _kwargs : None )
197+ monkeypatch .setattr (signal , "setitimer" , lambda * _args , ** _kwargs : None )
198+ monkeypatch .setitem (sys .modules , "resource" , _DummyResource )
199+
200+ # Should not raise even if restoring old limits fails.
201+ with extractor ._parse_limits (5 ):
202+ pass
203+
204+
109205def test_extract_syntax_error () -> None :
110206 with pytest .raises (ParseError ):
111207 extract_units_from_source (
0 commit comments