Skip to content
Merged
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
11 changes: 11 additions & 0 deletions src/grizzly/replay/replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,17 @@ def _setup_server_map(self, services: WebServices | None = None) -> ServerMap:
server_map.set_redirect("grz_start", "grz_harness", required=False)
if services:
services.map_locations(server_map)
# expose the current browser logs over HTTP
server_map.set_dynamic_response(
"grz_stderr",
lambda _: self.target.read_log("stderr"),
mime_type="text/plain",
)
server_map.set_dynamic_response(
"grz_stdout",
lambda _: self.target.read_log("stdout"),
mime_type="text/plain",
)
return server_map

def _process_reports(
Expand Down
27 changes: 18 additions & 9 deletions src/grizzly/replay/test_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -1025,32 +1025,41 @@ def test_replay_29(mocker, server):
target = mocker.Mock(spec_set=Target, closed=True, launch_timeout=30)
sm_cls = mocker.patch("grizzly.replay.replay.ServerMap", autospec=True)

def mapped_responses():
# map of registered dynamic response url -> (callback, kwargs)
return {
call.args[0]: (call.args[1], call.kwargs)
for call in sm_cls.return_value.set_dynamic_response.call_args_list
}

# without harness - redirects directly to current test
with ReplayManager([], server, target, use_harness=False) as replay:
result = replay._setup_server_map()
assert result is sm_cls.return_value
sm_cls.return_value.set_redirect.assert_called_once_with(
"grz_start", "grz_current_test", required=False
)
sm_cls.return_value.set_dynamic_response.assert_not_called()
# only the browser log endpoints are registered (no harness)
assert set(mapped_responses()) == {"grz_stderr", "grz_stdout"}
sm_cls.reset_mock()

# with harness - serves harness file and redirects to it
with ReplayManager([], server, target, use_harness=True) as replay:
harness_content = replay._harness
result = replay._setup_server_map()
assert result is sm_cls.return_value
sm_cls.return_value.set_dynamic_response.assert_called_once()
name, fn = sm_cls.return_value.set_dynamic_response.call_args[0]
assert name == "grz_harness"
assert (
sm_cls.return_value.set_dynamic_response.call_args[1]["mime_type"]
== "text/html"
)
assert fn(None) == harness_content
responses = mapped_responses()
assert responses["grz_harness"][1]["mime_type"] == "text/html"
assert responses["grz_harness"][0](None) == harness_content
sm_cls.return_value.set_redirect.assert_called_once_with(
"grz_start", "grz_harness", required=False
)
# browser log endpoints return the current logs as text/plain
for log in ("grz_stderr", "grz_stdout"):
assert responses[log][1]["mime_type"] == "text/plain"
target.read_log.return_value = b"log data"
assert responses["grz_stderr"][0](None) == b"log data"
target.read_log.assert_called_with("stderr")
sm_cls.reset_mock()

# with services - locations are mapped onto the server map
Expand Down
11 changes: 11 additions & 0 deletions src/grizzly/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,17 @@ def run(
)
if services:
services.map_locations(self.iomanager.server_map)
# expose the current browser logs over HTTP
self.iomanager.server_map.set_dynamic_response(
"grz_stderr",
lambda _: self.target.read_log("stderr"),
mime_type="text/plain",
)
self.iomanager.server_map.set_dynamic_response(
"grz_stdout",
lambda _: self.target.read_log("stdout"),
mime_type="text/plain",
)

log_limiter = LogOutputLimiter(rate=log_rate)
# limit relaunch to max iterations if needed
Expand Down
9 changes: 9 additions & 0 deletions src/grizzly/target/firefox_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,15 @@ def log_size(self) -> int:
total += length
return total

def read_log(self, log_id: str) -> bytes:
cloned = self._puppet.clone_log(log_id)
if cloned is None:
return b""
try:
return cloned.read_bytes()
finally:
cloned.unlink(missing_ok=True)

def merge_environment(self, extra: Mapping[str, str]) -> None:
# use extra as base
output = dict(extra)
Expand Down
11 changes: 11 additions & 0 deletions src/grizzly/target/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@ def log_size(self) -> int:
Total data size of log files in bytes.
"""

def read_log(self, log_id: str) -> bytes: # pylint: disable=unused-argument
"""Return the current contents of a browser log (e.g. 'stdout', 'stderr').

Args:
log_id: Log identifier.

Returns:
Log contents, or empty bytes if unavailable.
"""
return b""

@abstractmethod
def merge_environment(self, extra: Mapping[str, str]) -> None:
"""Add to existing environment.
Expand Down
17 changes: 17 additions & 0 deletions src/grizzly/target/test_firefox_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,20 @@ def test_firefox_target_16(mocker, tmp_path):
)
FirefoxTarget._get_certdb(tmp_path / "root.pem", "fake-certutil")
assert fake_add.call_count == 1


def test_firefox_target_17(mocker, tmp_path):
"""test FirefoxTarget.read_log()"""
fake_ffp = mocker.patch("grizzly.target.firefox_target.FFPuppet", autospec=True)
with FirefoxTarget(tmp_path / "fake", 300, 25, 5000) as target:
# log available
cloned = tmp_path / "cloned.txt"
cloned.write_bytes(b"hello stderr")
fake_ffp.return_value.clone_log.return_value = cloned
assert target.read_log("stderr") == b"hello stderr"
fake_ffp.return_value.clone_log.assert_called_once_with("stderr")
# cloned copy is removed after reading
assert not cloned.is_file()
# log unavailable
fake_ffp.return_value.clone_log.return_value = None
assert target.read_log("stdout") == b""
1 change: 1 addition & 0 deletions src/grizzly/target/test_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def test_target_01(tmp_path):
assert target.memory_limit == 3
# test stubs
target.reverse(1, 2)
assert target.read_log("stderr") == b""


def test_target_02(mocker, tmp_path):
Expand Down
3 changes: 3 additions & 0 deletions src/grizzly/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ def test_session_01(mocker, harness, profiling, coverage, relaunch, iters, runti
assert target.close.call_count == max_iters / relaunch
assert target.check_result.call_count == max_iters
assert target.handle_hang.call_count == 0
# browser log endpoints are exposed
for log in ("grz_stderr", "grz_stdout"):
assert session.iomanager.server_map.dynamic[log].mime == "text/plain"
if profiling:
assert any(session.status.profile_entries()) == profiling
else:
Expand Down
Loading