diff --git a/src/testcontainers/core/wait_strategies.py b/src/testcontainers/core/wait_strategies.py index 4e9f5a77..29938bb2 100644 --- a/src/testcontainers/core/wait_strategies.py +++ b/src/testcontainers/core/wait_strategies.py @@ -127,9 +127,9 @@ def wait_until_ready(self, container: "WaitStrategyTarget") -> None: stderr = stderr_bytes.decode() predicate_result = ( - self._message.search(stdout) or self._message.search(stderr) + self._check_message_occurs_n_times(stdout) or self._check_message_occurs_n_times(stderr) if self._predicate_streams_and is False - else self._message.search(stdout) and self._message.search(stderr) + else self._check_message_occurs_n_times(stdout) and self._check_message_occurs_n_times(stderr) ) if predicate_result: @@ -151,6 +151,15 @@ def wait_until_ready(self, container: "WaitStrategyTarget") -> None: time.sleep(self._poll_interval) + def _check_message_occurs_n_times(self, buffer: str) -> bool: + """Check if buffer contains as many occurrences of message as required""" + import itertools + + # Limit the number of searches finditer does to self._times + matches_iter = itertools.islice(self._message.finditer(buffer), self._times) + # len(list(matches_iter)) would be simpler but constructs an unused list of matches + return sum(1 for _ in matches_iter) >= self._times + class HttpWaitStrategy(WaitStrategy): """ diff --git a/tests/core/test_wait_strategies.py b/tests/core/test_wait_strategies.py index 60c1be57..5bd91881 100644 --- a/tests/core/test_wait_strategies.py +++ b/tests/core/test_wait_strategies.py @@ -118,13 +118,15 @@ def test_log_message_wait_strategy_initialization(self, message, times, predicat assert strategy._predicate_streams_and is predicate_streams_and @pytest.mark.parametrize( - "container_logs,expected_message,should_succeed", + "container_logs,expected_message,times,should_succeed", [ - ((b"test message", b""), "test message", True), - ((b"", b"test message"), "test message", True), - ((b"no match", b""), "test message", False), - ((b"test123", b""), re.compile(r"test\d+"), True), - ((b"test", b""), re.compile(r"test\d+"), False), + ((b"test message", b""), "test message", 1, True), + ((b"", b"test message"), "test message", 1, True), + ((b"no match", b""), "test message", 1, False), + ((b"test123", b""), re.compile(r"test\d+"), 1, True), + ((b"test", b""), re.compile(r"test\d+"), 1, False), + ((b"test", b""), re.compile(r"test"), 2, False), + ((b"test test", b""), re.compile(r"test"), 2, True), ], ids=[ "stdout_contains_message_success", @@ -132,12 +134,14 @@ def test_log_message_wait_strategy_initialization(self, message, times, predicat "no_message_match_failure", "regex_pattern_match_success", "regex_pattern_no_match_failure", + "message_found_once_but_required_twice_failure", + "message_found_twice_as_required_success", ], ) @patch("time.time") @patch("time.sleep") - def test_wait_until_ready(self, mock_sleep, mock_time, container_logs, expected_message, should_succeed): - strategy = LogMessageWaitStrategy(expected_message) + def test_wait_until_ready(self, mock_sleep, mock_time, container_logs, expected_message, times, should_succeed): + strategy = LogMessageWaitStrategy(expected_message, times=times) mock_container = Mock() mock_container.get_logs.return_value = container_logs # Mock the wrapped container to simulate a running container