Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/testcontainers/core/wait_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
"""
Expand Down
20 changes: 12 additions & 8 deletions tests/core/test_wait_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,26 +118,30 @@ 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",
"stderr_contains_message_success",
"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
Expand Down