Commit 3c129283 authored by Cole Winstanley's avatar Cole Winstanley Committed by Commit Bot

[ChromeDriver] Implement DevTools-side log-replay clients

Implemented the connections between the C++ DevToolsLogReader and the
DevTools client classes DevToolsClientImpl and DevToolsHttpClient. This
Allows ChromeDriver to "communicate with" a log file, thinking it is
communicating with an actual instance of Chrome.

Bug: chromedriver:2501
Change-Id: I7aa55500ccd55c349bdd8c1a96952fa8570bec39
Reviewed-on: https://chromium-review.googlesource.com/1178985
Commit-Queue: Cole Winstanley <cwinstanley@google.com>
Reviewed-by: default avatarJohn Chen <johnchen@chromium.org>
Reviewed-by: default avatarCaleb Rouleau <crouleau@chromium.org>
Cr-Commit-Position: refs/heads/master@{#586441}
parent 8f06b391
...@@ -150,8 +150,14 @@ source_set("automation_client_lib") { ...@@ -150,8 +150,14 @@ source_set("automation_client_lib") {
"chrome/web_view.h", "chrome/web_view.h",
"chrome/web_view_impl.cc", "chrome/web_view_impl.cc",
"chrome/web_view_impl.h", "chrome/web_view_impl.h",
"log_replay/chrome_replay_impl.cc",
"log_replay/chrome_replay_impl.h",
"log_replay/devtools_log_reader.cc", "log_replay/devtools_log_reader.cc",
"log_replay/devtools_log_reader.h", "log_replay/devtools_log_reader.h",
"log_replay/log_replay_socket.cc",
"log_replay/log_replay_socket.h",
"log_replay/replay_http_client.cc",
"log_replay/replay_http_client.h",
"net/adb_client_socket.cc", "net/adb_client_socket.cc",
"net/adb_client_socket.h", "net/adb_client_socket.h",
"net/net_util.cc", "net/net_util.cc",
......
...@@ -98,7 +98,9 @@ DevToolsClientImpl::DevToolsClientImpl(const SyncWebSocketFactory& factory, ...@@ -98,7 +98,9 @@ DevToolsClientImpl::DevToolsClientImpl(const SyncWebSocketFactory& factory,
parser_func_(base::Bind(&internal::ParseInspectorMessage)), parser_func_(base::Bind(&internal::ParseInspectorMessage)),
unnotified_event_(NULL), unnotified_event_(NULL),
next_id_(1), next_id_(1),
stack_count_(0) {} stack_count_(0) {
socket_->SetId(id_);
}
DevToolsClientImpl::DevToolsClientImpl( DevToolsClientImpl::DevToolsClientImpl(
const SyncWebSocketFactory& factory, const SyncWebSocketFactory& factory,
...@@ -116,7 +118,9 @@ DevToolsClientImpl::DevToolsClientImpl( ...@@ -116,7 +118,9 @@ DevToolsClientImpl::DevToolsClientImpl(
parser_func_(base::Bind(&internal::ParseInspectorMessage)), parser_func_(base::Bind(&internal::ParseInspectorMessage)),
unnotified_event_(NULL), unnotified_event_(NULL),
next_id_(1), next_id_(1),
stack_count_(0) {} stack_count_(0) {
socket_->SetId(id_);
}
DevToolsClientImpl::DevToolsClientImpl(DevToolsClientImpl* parent, DevToolsClientImpl::DevToolsClientImpl(DevToolsClientImpl* parent,
const std::string& session_id) const std::string& session_id)
...@@ -151,7 +155,9 @@ DevToolsClientImpl::DevToolsClientImpl( ...@@ -151,7 +155,9 @@ DevToolsClientImpl::DevToolsClientImpl(
parser_func_(parser_func), parser_func_(parser_func),
unnotified_event_(NULL), unnotified_event_(NULL),
next_id_(1), next_id_(1),
stack_count_(0) {} stack_count_(0) {
socket_->SetId(id_);
}
DevToolsClientImpl::~DevToolsClientImpl() { DevToolsClientImpl::~DevToolsClientImpl() {
if (parent_ != nullptr) if (parent_ != nullptr)
...@@ -313,7 +319,7 @@ Status DevToolsClientImpl::SendCommandInternal( ...@@ -313,7 +319,7 @@ Status DevToolsClientImpl::SendCommandInternal(
// Note: ChromeDriver log-replay depends on the format of this logging. // Note: ChromeDriver log-replay depends on the format of this logging.
// see chromedriver/log_replay/devtools_log_reader.cc. // see chromedriver/log_replay/devtools_log_reader.cc.
VLOG(1) << "DevTools WebSocket Command: " << method << " (id=" << command_id VLOG(1) << "DevTools WebSocket Command: " << method << " (id=" << command_id
<< ") " << FormatValueForDisplay(params); << ") " << id_ << " " << FormatValueForDisplay(params);
} }
if (parent_ != nullptr) { if (parent_ != nullptr) {
base::DictionaryValue params2; base::DictionaryValue params2;
...@@ -437,7 +443,7 @@ Status DevToolsClientImpl::ProcessEvent(const internal::InspectorEvent& event) { ...@@ -437,7 +443,7 @@ Status DevToolsClientImpl::ProcessEvent(const internal::InspectorEvent& event) {
if (IsVLogOn(1)) { if (IsVLogOn(1)) {
// Note: ChromeDriver log-replay depends on the format of this logging. // Note: ChromeDriver log-replay depends on the format of this logging.
// see chromedriver/log_replay/devtools_log_reader.cc. // see chromedriver/log_replay/devtools_log_reader.cc.
VLOG(1) << "DevTools WebSocket Event: " << event.method << " " VLOG(1) << "DevTools WebSocket Event: " << event.method << " " << id_ << " "
<< FormatValueForDisplay(*event.params); << FormatValueForDisplay(*event.params);
} }
unnotified_event_listeners_ = listeners_; unnotified_event_listeners_ = listeners_;
...@@ -514,7 +520,7 @@ Status DevToolsClientImpl::ProcessCommandResponse( ...@@ -514,7 +520,7 @@ Status DevToolsClientImpl::ProcessCommandResponse(
// Note: ChromeDriver log-replay depends on the format of this logging. // Note: ChromeDriver log-replay depends on the format of this logging.
// see chromedriver/log_replay/devtools_log_reader.cc. // see chromedriver/log_replay/devtools_log_reader.cc.
VLOG(1) << "DevTools WebSocket Response: " << method VLOG(1) << "DevTools WebSocket Response: " << method
<< " (id=" << response.id << ") " << result; << " (id=" << response.id << ") " << id_ << " " << result;
} }
if (iter == response_info_map_.end()) if (iter == response_info_map_.end())
......
...@@ -80,7 +80,7 @@ class DevToolsHttpClient { ...@@ -80,7 +80,7 @@ class DevToolsHttpClient {
std::unique_ptr<DeviceMetrics> device_metrics, std::unique_ptr<DeviceMetrics> device_metrics,
std::unique_ptr<std::set<WebViewInfo::Type>> window_types, std::unique_ptr<std::set<WebViewInfo::Type>> window_types,
std::string page_load_strategy); std::string page_load_strategy);
~DevToolsHttpClient(); virtual ~DevToolsHttpClient();
Status Init(const base::TimeDelta& timeout); Status Init(const base::TimeDelta& timeout);
...@@ -98,9 +98,9 @@ class DevToolsHttpClient { ...@@ -98,9 +98,9 @@ class DevToolsHttpClient {
private: private:
Status CloseFrontends(const std::string& for_client_id); Status CloseFrontends(const std::string& for_client_id);
bool FetchUrlAndLog(const std::string& url, virtual bool FetchUrlAndLog(const std::string& url,
URLRequestContextGetter* getter, URLRequestContextGetter* getter,
std::string* response); std::string* response);
scoped_refptr<URLRequestContextGetter> context_getter_; scoped_refptr<URLRequestContextGetter> context_getter_;
SyncWebSocketFactory socket_factory_; SyncWebSocketFactory socket_factory_;
......
...@@ -48,6 +48,8 @@ ...@@ -48,6 +48,8 @@
#include "chrome/test/chromedriver/chrome/user_data_dir.h" #include "chrome/test/chromedriver/chrome/user_data_dir.h"
#include "chrome/test/chromedriver/chrome/version.h" #include "chrome/test/chromedriver/chrome/version.h"
#include "chrome/test/chromedriver/chrome/web_view.h" #include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/log_replay/chrome_replay_impl.h"
#include "chrome/test/chromedriver/log_replay/replay_http_client.h"
#include "chrome/test/chromedriver/net/net_util.h" #include "chrome/test/chromedriver/net/net_util.h"
#include "chrome/test/chromedriver/net/url_request_context_getter.h" #include "chrome/test/chromedriver/net/url_request_context_getter.h"
#include "crypto/rsa_private_key.h" #include "crypto/rsa_private_key.h"
...@@ -212,9 +214,22 @@ Status WaitForDevToolsAndCheckVersion( ...@@ -212,9 +214,22 @@ Status WaitForDevToolsAndCheckVersion(
window_types.reset(new std::set<WebViewInfo::Type>()); window_types.reset(new std::set<WebViewInfo::Type>());
} }
std::unique_ptr<DevToolsHttpClient> client(new DevToolsHttpClient( std::unique_ptr<DevToolsHttpClient> client;
address, context_getter, socket_factory, std::move(device_metrics), base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
std::move(window_types), capabilities->page_load_strategy)); if (cmd_line->HasSwitch("devtools-replay")) {
base::CommandLine::StringType log_path =
cmd_line->GetSwitchValueNative("devtools-replay");
base::FilePath log_file_path(log_path);
client.reset(
new ReplayHttpClient(address, context_getter, socket_factory,
std::move(device_metrics), std::move(window_types),
capabilities->page_load_strategy, log_file_path));
} else {
client.reset(new DevToolsHttpClient(
address, context_getter, socket_factory, std::move(device_metrics),
std::move(window_types), capabilities->page_load_strategy));
}
base::TimeTicks deadline = base::TimeTicks deadline =
base::TimeTicks::Now() + base::TimeDelta::FromSeconds(wait_time); base::TimeTicks::Now() + base::TimeDelta::FromSeconds(wait_time);
Status status = client->Init(deadline - base::TimeTicks::Now()); Status status = client->Init(deadline - base::TimeTicks::Now());
...@@ -230,7 +245,6 @@ Status WaitForDevToolsAndCheckVersion( ...@@ -230,7 +245,6 @@ Status WaitForDevToolsAndCheckVersion(
browser_info->android_package.c_str())); browser_info->android_package.c_str()));
} }
base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
if (cmd_line->HasSwitch("disable-build-check")) { if (cmd_line->HasSwitch("disable-build-check")) {
LOG(WARNING) << "You are using an unsupported command-line switch: " LOG(WARNING) << "You are using an unsupported command-line switch: "
"--disable-build-check. Please don't report bugs that " "--disable-build-check. Please don't report bugs that "
...@@ -458,15 +472,15 @@ Status LaunchDesktopChrome(URLRequestContextGetter* context_getter, ...@@ -458,15 +472,15 @@ Status LaunchDesktopChrome(URLRequestContextGetter* context_getter,
// is to do an exec of Chrome at the end of the script so that Chrome // is to do an exec of Chrome at the end of the script so that Chrome
// remains a subprocess of ChromeDriver. This allows us to have the // remains a subprocess of ChromeDriver. This allows us to have the
// correct process handle so that we can terminate Chrome after the // correct process handle so that we can terminate Chrome after the
// test has finished or in the case of any failure. If you can't exec the // test has finished or in the case of any failure. If you can't exec
// Chrome binary at the end of your script, you must find a way to // the Chrome binary at the end of your script, you must find a way to
// properly handle our termination signal so that you don't have zombie // properly handle our termination signal so that you don't have zombie
// Chrome processes running after the test is completed. // Chrome processes running after the test is completed.
failure_status.AddDetails( failure_status.AddDetails("The process started from chrome location " +
"The process started from chrome location " + command.GetProgram().AsUTF8Unsafe() +
command.GetProgram().AsUTF8Unsafe() + " is no longer running, so ChromeDriver is "
" is no longer running, so ChromeDriver is assuming that Chrome has " "assuming that Chrome has "
"crashed."); "crashed.");
return failure_status; return failure_status;
} }
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50)); base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
...@@ -494,11 +508,12 @@ Status LaunchDesktopChrome(URLRequestContextGetter* context_getter, ...@@ -494,11 +508,12 @@ Status LaunchDesktopChrome(URLRequestContextGetter* context_getter,
<< status.message(); << status.message();
} }
std::unique_ptr<ChromeDesktopImpl> chrome_desktop(new ChromeDesktopImpl( std::unique_ptr<ChromeDesktopImpl> chrome_desktop =
std::move(devtools_http_client), std::move(devtools_websocket_client), std::make_unique<ChromeDesktopImpl>(
std::move(devtools_event_listeners), capabilities.page_load_strategy, std::move(devtools_http_client), std::move(devtools_websocket_client),
std::move(process), command, &user_data_dir_temp_dir, &extension_dir, std::move(devtools_event_listeners), capabilities.page_load_strategy,
capabilities.network_emulation_enabled)); std::move(process), command, &user_data_dir_temp_dir, &extension_dir,
capabilities.network_emulation_enabled);
if (!capabilities.extension_load_timeout.is_zero()) { if (!capabilities.extension_load_timeout.is_zero()) {
for (size_t i = 0; i < extension_bg_pages.size(); ++i) { for (size_t i = 0; i < extension_bg_pages.size(); ++i) {
VLOG(0) << "Waiting for extension bg page load: " VLOG(0) << "Waiting for extension bg page load: "
...@@ -582,6 +597,76 @@ Status LaunchAndroidChrome(URLRequestContextGetter* context_getter, ...@@ -582,6 +597,76 @@ Status LaunchAndroidChrome(URLRequestContextGetter* context_getter,
return Status(kOk); return Status(kOk);
} }
Status LaunchReplayChrome(URLRequestContextGetter* context_getter,
const SyncWebSocketFactory& socket_factory,
const Capabilities& capabilities,
std::vector<std::unique_ptr<DevToolsEventListener>>
devtools_event_listeners,
std::unique_ptr<Chrome>* chrome,
bool w3c_compliant) {
base::CommandLine command(base::CommandLine::NO_PROGRAM);
base::ScopedTempDir user_data_dir_temp_dir;
base::ScopedTempDir extension_dir;
Status status = Status(kOk);
std::vector<std::string> extension_bg_pages;
if (capabilities.switches.HasSwitch("user-data-dir")) {
status = internal::RemoveOldDevToolsActivePortFile(base::FilePath(
capabilities.switches.GetSwitchValueNative("user-data-dir")));
if (status.IsError()) {
return status;
}
}
#if defined(OS_WIN)
if (!SwitchToUSKeyboardLayout())
VLOG(0) << "Cannot switch to US keyboard layout - some keys may be "
"interpreted incorrectly";
#endif
std::unique_ptr<DevToolsHttpClient> devtools_http_client;
status = WaitForDevToolsAndCheckVersion(NetAddress(0), context_getter,
socket_factory, &capabilities, 1,
&devtools_http_client);
std::unique_ptr<DevToolsClient> devtools_websocket_client;
status = CreateBrowserwideDevToolsClientAndConnect(
NetAddress(0), capabilities.perf_logging_prefs, socket_factory,
devtools_event_listeners,
devtools_http_client->browser_info()->web_socket_url,
&devtools_websocket_client);
if (status.IsError()) {
LOG(WARNING) << "Browser-wide DevTools client failed to connect: "
<< status.message();
}
base::Process dummy_process;
std::unique_ptr<ChromeDesktopImpl> chrome_impl =
std::make_unique<ChromeReplayImpl>(
std::move(devtools_http_client), std::move(devtools_websocket_client),
std::move(devtools_event_listeners), capabilities.page_load_strategy,
std::move(dummy_process), command, &user_data_dir_temp_dir,
&extension_dir, capabilities.network_emulation_enabled);
if (!capabilities.extension_load_timeout.is_zero()) {
for (size_t i = 0; i < extension_bg_pages.size(); ++i) {
VLOG(0) << "Waiting for extension bg page load: "
<< extension_bg_pages[i];
std::unique_ptr<WebView> web_view;
Status status = chrome_impl->WaitForPageToLoad(
extension_bg_pages[i], capabilities.extension_load_timeout, &web_view,
w3c_compliant);
if (status.IsError()) {
return Status(kUnknownError,
"failed to wait for extension background page to load: " +
extension_bg_pages[i],
status);
}
}
}
*chrome = std::move(chrome_impl);
return Status(kOk);
}
} // namespace } // namespace
Status LaunchChrome(URLRequestContextGetter* context_getter, Status LaunchChrome(URLRequestContextGetter* context_getter,
...@@ -599,10 +684,15 @@ Status LaunchChrome(URLRequestContextGetter* context_getter, ...@@ -599,10 +684,15 @@ Status LaunchChrome(URLRequestContextGetter* context_getter,
context_getter, socket_factory, capabilities, context_getter, socket_factory, capabilities,
std::move(devtools_event_listeners), chrome); std::move(devtools_event_listeners), chrome);
} }
const base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
if (capabilities.IsAndroid()) { if (capabilities.IsAndroid()) {
return LaunchAndroidChrome(context_getter, socket_factory, capabilities, return LaunchAndroidChrome(context_getter, socket_factory, capabilities,
std::move(devtools_event_listeners), std::move(devtools_event_listeners),
device_manager, chrome); device_manager, chrome);
} else if (cmd_line->HasSwitch("devtools-replay")) {
return LaunchReplayChrome(context_getter, socket_factory, capabilities,
std::move(devtools_event_listeners), chrome,
w3c_compliant);
} else { } else {
return LaunchDesktopChrome(context_getter, socket_factory, capabilities, return LaunchDesktopChrome(context_getter, socket_factory, capabilities,
std::move(devtools_event_listeners), chrome, std::move(devtools_event_listeners), chrome,
......
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/chromedriver/log_replay/chrome_replay_impl.h"
#include "chrome/test/chromedriver/chrome/devtools_client.h"
#include "chrome/test/chromedriver/chrome/devtools_event_listener.h"
#include "chrome/test/chromedriver/chrome/devtools_http_client.h"
#include "chrome/test/chromedriver/chrome/status.h"
ChromeReplayImpl::ChromeReplayImpl(
std::unique_ptr<DevToolsHttpClient> http_client,
std::unique_ptr<DevToolsClient> websocket_client,
std::vector<std::unique_ptr<DevToolsEventListener>>
devtools_event_listeners,
std::string page_load_strategy,
base::Process process,
const base::CommandLine& command,
base::ScopedTempDir* user_data_dir,
base::ScopedTempDir* extension_dir,
bool network_emulation_enabled)
: ChromeDesktopImpl(std::move(http_client),
std::move(websocket_client),
std::move(devtools_event_listeners),
page_load_strategy,
std::move(process),
command,
user_data_dir,
extension_dir,
network_emulation_enabled) {}
ChromeReplayImpl::~ChromeReplayImpl() {}
Status ChromeReplayImpl::QuitImpl() {
return Status(kOk);
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_TEST_CHROMEDRIVER_LOG_REPLAY_CHROME_REPLAY_IMPL_H_
#define CHROME_TEST_CHROMEDRIVER_LOG_REPLAY_CHROME_REPLAY_IMPL_H_
#include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h"
class DevToolsClient;
class DevToolsHttpClient;
class Status;
// Same as ChromeDesktopImpl except that it completely ignores the existence
// of the |process| passed into the constructor. This allows running Chrome
// with a connection to log-replay DevTools implementations without launching
// a Chrome process at all.
class ChromeReplayImpl : public ChromeDesktopImpl {
public:
ChromeReplayImpl(std::unique_ptr<DevToolsHttpClient> http_client,
std::unique_ptr<DevToolsClient> websocket_client,
std::vector<std::unique_ptr<DevToolsEventListener>>
devtools_event_listeners,
std::string page_load_strategy,
base::Process process,
const base::CommandLine& command,
base::ScopedTempDir* user_data_dir,
base::ScopedTempDir* extension_dir,
bool network_emulation_enabled);
~ChromeReplayImpl() override;
// A no-op: all this does in DesktopChromeImpl is kill the Chrome process.
Status QuitImpl() override;
};
#endif // CHROME_TEST_CHROMEDRIVER_LOG_REPLAY_CHROME_REPLAY_IMPL_H_
...@@ -699,6 +699,7 @@ class CommandSequence(object): ...@@ -699,6 +699,7 @@ class CommandSequence(object):
self._parser = _ParserWithUndo(log_path) self._parser = _ParserWithUndo(log_path)
self._staged_logged_ids = None self._staged_logged_ids = None
self._staged_logged_session_id = None self._staged_logged_session_id = None
self._last_response = None
def NextCommand(self, previous_response): def NextCommand(self, previous_response):
"""Get the next command in the log file. """Get the next command in the log file.
...@@ -865,18 +866,22 @@ class Replayer(object): ...@@ -865,18 +866,22 @@ class Replayer(object):
command.GetPayloadPrimitive()) command.GetPayloadPrimitive())
def StartChromeDriverServer(chromedriver_binary, options): def StartChromeDriverServer(chromedriver_binary,
output_log_path,
devtools_replay_path="",
replayable=False):
chromedriver = util.GetAbsolutePathOfUserPath(chromedriver_binary) chromedriver = util.GetAbsolutePathOfUserPath(chromedriver_binary)
if (not os.path.exists(chromedriver) and if (not os.path.exists(chromedriver) and
util.GetPlatformName() == "win" and util.GetPlatformName() == "win" and
not chromedriver.lower().endswith(".exe")): not chromedriver.lower().endswith(".exe")):
chromedriver = chromedriver + ".exe" chromedriver = chromedriver + ".exe"
if options.output_log_path: if output_log_path:
options.output_log_path = util.GetAbsolutePathOfUserPath( output_log_path = util.GetAbsolutePathOfUserPath(output_log_path)
options.output_log_path)
chromedriver_server = server.Server(chromedriver_binary, chromedriver_server = server.Server(chromedriver_binary,
log_path=options.output_log_path) log_path=output_log_path,
devtools_replay_path=devtools_replay_path,
replayable=replayable)
return chromedriver_server return chromedriver_server
...@@ -892,23 +897,35 @@ def _GetCommandLineOptions(): ...@@ -892,23 +897,35 @@ def _GetCommandLineOptions():
"", "--chrome", help="Path to a build of the chrome binary. If not\n" "", "--chrome", help="Path to a build of the chrome binary. If not\n"
"specified, uses ChromeDriver's own algorithm to find Chrome.") "specified, uses ChromeDriver's own algorithm to find Chrome.")
parser.add_option( parser.add_option(
"", "--base_url", help="Base url to replace logged urls (in " "", "--base-url", help="Base url to replace logged urls (in "
"navigate, getUrl, and similar commands/responses).") "navigate, getUrl, and similar commands/responses).")
parser.add_option(
"", "--devtools-replay", help="Replay DevTools actions in addition\n"
"to client-side actions")
# TODO(crbug.com/chromedriver/2501)
parser.add_option(
"", "--replayable", help="Generate logs that do not have truncated\n"
"strings so that they can be replayed again.")
options, args = parser.parse_args() options, args = parser.parse_args()
if not os.path.exists(args[0]): if not os.path.exists(args[0]):
parser.error("Path given by --chromedriver is invalid.\n" parser.error("Path given for chromedriver is invalid.\n"
'Please run "%s --help" for help' % __file__) 'Please run "%s --help" for help' % __file__)
if options.chrome and not os.path.exists(options.chrome): if options.chrome and not os.path.exists(options.chrome):
parser.error("Path given by --chrome is invalid.\n" parser.error("Path given by --chrome is invalid.\n"
'Please run "%s --help" for help' % __file__) 'Please run "%s --help" for help' % __file__)
if options.replayable and not options.output_log_path:
parser.error("Replayable log option needs --output-log-path specified. \n"
'Please run "%s --help" for help' % __file__)
return options, args return options, args
def main(): def main():
options, args = _GetCommandLineOptions() options, args = _GetCommandLineOptions()
server = StartChromeDriverServer(args[0], options) devtools_replay_path = args[1] if options.devtools_replay else None
server = StartChromeDriverServer(args[0], options.output_log_path,
devtools_replay_path, options.replayable)
input_log_path = util.GetAbsolutePathOfUserPath(args[1]) input_log_path = util.GetAbsolutePathOfUserPath(args[1])
chrome_binary = (util.GetAbsolutePathOfUserPath(options.chrome) chrome_binary = (util.GetAbsolutePathOfUserPath(options.chrome)
if options.chrome else None) if options.chrome else None)
......
...@@ -47,7 +47,7 @@ def SubstituteVariableEntries(s): ...@@ -47,7 +47,7 @@ def SubstituteVariableEntries(s):
def ClearPort(s): def ClearPort(s):
"""Removes port numbers from urls in the given string.""" """Removes port numbers from urls in the given string."""
s = re.sub(r":([0-9]){5}/", "<port>", s) s = re.sub(r":([0-9]){5}/", "<port>", s)
return re.sub(r"localhost:([0-9]){5}", "localhost:<port>", s) return re.sub(r"localhost:([0-9]*)", "localhost:<port>", s)
class ChromeDriverClientReplayTest(unittest.TestCase): class ChromeDriverClientReplayTest(unittest.TestCase):
...@@ -113,9 +113,12 @@ class ChromeDriverClientReplayTest(unittest.TestCase): ...@@ -113,9 +113,12 @@ class ChromeDriverClientReplayTest(unittest.TestCase):
log_file_relative_path: relative path (from this the directory this file log_file_relative_path: relative path (from this the directory this file
is in) to the log file for this test as a string. is in) to the log file for this test as a string.
""" """
log_file = os.path.join(_THIS_DIR, log_file_relative_path) log_path = os.path.join(_THIS_DIR, log_file_relative_path)
with open(log_file) as lf: with open(log_path) as lf:
server = client_replay.StartChromeDriverServer(_CHROMEDRIVER, _OPTIONS) replay_path = log_path if _OPTIONS.devtools_replay else ""
server = client_replay.StartChromeDriverServer(_CHROMEDRIVER,
_OPTIONS.output_log_path,
replay_path)
chrome_binary = (util.GetAbsolutePathOfUserPath(_OPTIONS.chrome) chrome_binary = (util.GetAbsolutePathOfUserPath(_OPTIONS.chrome)
if _OPTIONS.chrome else None) if _OPTIONS.chrome else None)
...@@ -136,7 +139,7 @@ class ChromeDriverClientReplayTest(unittest.TestCase): ...@@ -136,7 +139,7 @@ class ChromeDriverClientReplayTest(unittest.TestCase):
server.Kill() server.Kill()
def testGetPageSource(self): def testGetPageSource(self):
self.runTest("test_data/testPageSource.log") self.runTest("test_data/testGetPageSource.log")
def testCloseWindow(self): def testCloseWindow(self):
self.runTest("test_data/testCloseWindow.log") self.runTest("test_data/testCloseWindow.log")
...@@ -150,11 +153,11 @@ class ChromeDriverClientReplayTest(unittest.TestCase): ...@@ -150,11 +153,11 @@ class ChromeDriverClientReplayTest(unittest.TestCase):
def testSendingTabKeyMovesToNextInputElement(self): def testSendingTabKeyMovesToNextInputElement(self):
self.runTest("test_data/testSendingTabKeyMovesToNextInputElement.log") self.runTest("test_data/testSendingTabKeyMovesToNextInputElement.log")
def testUnexpectedalertBehavior(self): def testUnexpectedAlertBehaviour(self):
self.runTest("test_data/testUnexpectedAlertBehavior.log") self.runTest("test_data/testUnexpectedAlertBehaviour.log")
def testDownload(self): def testDownload(self):
self.runTest("test_data/testDownload.log") self.runTest("test_data/testFileDownloadWithClick.log")
def testCanSwitchToPrintPreviewDialog(self): def testCanSwitchToPrintPreviewDialog(self):
self.runTest("test_data/testCanSwitchToPrintPreviewDialog.log") self.runTest("test_data/testCanSwitchToPrintPreviewDialog.log")
...@@ -163,10 +166,10 @@ class ChromeDriverClientReplayTest(unittest.TestCase): ...@@ -163,10 +166,10 @@ class ChromeDriverClientReplayTest(unittest.TestCase):
self.runTest("test_data/testClearElement.log") self.runTest("test_data/testClearElement.log")
def testEmulateNetwork(self): def testEmulateNetwork(self):
self.runTest("test_data/testEmulateNetwork.log") self.runTest("test_data/testEmulateNetworkConditions.log")
def testSwitchTo(self): def testSwitchTo(self):
self.runTest("test_data/testSwitchTo.log") self.runTest("test_data/testSwitchToWindow.log")
def testEvaluateScript(self): def testEvaluateScript(self):
self.runTest("test_data/testEvaluateScript.log") self.runTest("test_data/testEvaluateScript.log")
...@@ -180,14 +183,23 @@ class ChromeDriverClientReplayTest(unittest.TestCase): ...@@ -180,14 +183,23 @@ class ChromeDriverClientReplayTest(unittest.TestCase):
def testSendCommand(self): def testSendCommand(self):
self.runTest("test_data/testSendCommand.log") self.runTest("test_data/testSendCommand.log")
def testSessionHandling(self): def testGetSessions(self):
self.runTest("test_data/testSessionHandling.log") self.runTest("test_data/testGetSessions.log")
def testQuitASessionMoreThanOnce(self):
self.runTest("test_data/testQuitASessionMoreThanOnce.log")
def testCanClickOOPIF(self):
self.runTest("test_data/testCanClickOOPIF.log")
def testRunMethod(self): def testRunMethod(self):
"""Check the Replayer.run method, which the other tests bypass.""" """Check the Replayer.run method, which the other tests bypass."""
log_path = os.path.join(_THIS_DIR, "test_data/testGetTitle.log") log_path = os.path.join(_THIS_DIR, "test_data/testGetTitle.log")
with open(log_path) as lf: with open(log_path) as lf:
server = client_replay.StartChromeDriverServer(_CHROMEDRIVER, _OPTIONS) replay_path = log_path if _OPTIONS.devtools_replay else ""
server = client_replay.StartChromeDriverServer(_CHROMEDRIVER,
_OPTIONS.output_log_path,
replay_path)
chrome_binary = (util.GetAbsolutePathOfUserPath(_OPTIONS.chrome) chrome_binary = (util.GetAbsolutePathOfUserPath(_OPTIONS.chrome)
if _OPTIONS.chrome else None) if _OPTIONS.chrome else None)
...@@ -197,6 +209,14 @@ class ChromeDriverClientReplayTest(unittest.TestCase): ...@@ -197,6 +209,14 @@ class ChromeDriverClientReplayTest(unittest.TestCase):
def testChromeBinary(self): def testChromeBinary(self):
self.runTest("test_data/testChromeBinary.log") self.runTest("test_data/testChromeBinary.log")
def GetNegativeFilter(chrome_version):
if chrome_version in _VERSION_SPECIFIC_NEGATIVE_FILTER:
negative_filter = _VERSION_SPECIFIC_NEGATIVE_FILTER[chrome_version]
return "*-" + ":__main__.".join([""] + negative_filter)
return "*"
def main(): def main():
usage = "usage: %prog <chromedriver binary> [options]" usage = "usage: %prog <chromedriver binary> [options]"
parser = optparse.OptionParser(usage=usage) parser = optparse.OptionParser(usage=usage)
...@@ -205,10 +225,16 @@ def main(): ...@@ -205,10 +225,16 @@ def main():
help="Output verbose server logs to this file") help="Output verbose server logs to this file")
parser.add_option( parser.add_option(
"", "--chrome", help="Path to the desired Chrome binary.") "", "--chrome", help="Path to the desired Chrome binary.")
parser.add_option(
"", "--chrome-version", default="HEAD",
help="Version of Chrome. Default is 'HEAD'.")
parser.add_option( parser.add_option(
"", "--filter", type="string", default="*", "", "--filter", type="string", default="*",
help="Filter for specifying what tests to run, \"*\" will run all. E.g., " help="Filter for specifying what tests to run, \"*\" will run all,"
"*testRunMethod") "including tests excluded by default. E.g., *testRunMethod")
parser.add_option(
"", "--devtools-replay", help="Replay DevTools instead of using\n"
"real Chrome.")
global _OPTIONS, _CHROMEDRIVER global _OPTIONS, _CHROMEDRIVER
_OPTIONS, args = parser.parse_args() _OPTIONS, args = parser.parse_args()
......
...@@ -25,9 +25,9 @@ LogEntry::LogEntry(std::istringstream& header_stream) { ...@@ -25,9 +25,9 @@ LogEntry::LogEntry(std::istringstream& header_stream) {
std::string protocol_type_string; std::string protocol_type_string;
header_stream >> protocol_type_string; // "HTTP" or "WebSocket" header_stream >> protocol_type_string; // "HTTP" or "WebSocket"
if (protocol_type_string == "HTTP") { if (protocol_type_string == "HTTP") {
protocol_type = HTTP; protocol_type = kHTTP;
} else if (protocol_type_string == "WebSocket") { } else if (protocol_type_string == "WebSocket") {
protocol_type = WebSocket; protocol_type = kWebSocket;
} else { } else {
error = true; error = true;
LOG(ERROR) << "Could not read protocol from log entry header."; LOG(ERROR) << "Could not read protocol from log entry header.";
...@@ -38,31 +38,38 @@ LogEntry::LogEntry(std::istringstream& header_stream) { ...@@ -38,31 +38,38 @@ LogEntry::LogEntry(std::istringstream& header_stream) {
header_stream >> event_type_string; header_stream >> event_type_string;
if (event_type_string == "Response:") { if (event_type_string == "Response:") {
event_type = response; event_type = kResponse;
} else if (event_type_string == "Command:" || } else if (event_type_string == "Command:" ||
event_type_string == "Request:") { event_type_string == "Request:") {
event_type = request; event_type = kRequest;
} else if (event_type_string == "Event:") { } else if (event_type_string == "Event:") {
event_type = event; event_type = kEvent;
} else { } else {
error = true; error = true;
LOG(ERROR) << "Could not read event type from log entry header."; LOG(ERROR) << "Could not read event type from log entry header.";
return; return;
} }
if (!(protocol_type == HTTP && event_type == response)) { if (!(protocol_type == kHTTP && event_type == kResponse)) {
header_stream >> command_name; header_stream >> command_name;
if (command_name == "") { if (command_name == "") {
error = true; error = true;
LOG(ERROR) << "Could not read command name from log entry header"; LOG(ERROR) << "Could not read command name from log entry header";
return; return;
} }
if (protocol_type != HTTP) { if (protocol_type != kHTTP) {
id = GetId(header_stream); if (event_type != kEvent) {
if (id == 0) { id = GetId(header_stream);
if (id == 0) {
error = true;
LOG(ERROR) << "Could not read sequential id from log entry header.";
return;
}
}
header_stream >> socket_id;
if (socket_id == "") {
error = true; error = true;
LOG(ERROR) << "Could not read sequential id from log entry header."; LOG(ERROR) << "Could not read socket id from log entry header.";
return;
} }
} }
} }
...@@ -75,7 +82,7 @@ DevToolsLogReader::DevToolsLogReader(const base::FilePath& log_path) ...@@ -75,7 +82,7 @@ DevToolsLogReader::DevToolsLogReader(const base::FilePath& log_path)
DevToolsLogReader::~DevToolsLogReader() {} DevToolsLogReader::~DevToolsLogReader() {}
bool DevToolsLogReader::IsHeader(std::istringstream& header_stream) { bool DevToolsLogReader::IsHeader(std::istringstream& header_stream) const {
std::string word; std::string word;
header_stream >> word; // preamble header_stream >> word; // preamble
if (!base::MatchPattern(word, "[??????????.???][DEBUG]:")) { if (!base::MatchPattern(word, "[??????????.???][DEBUG]:")) {
...@@ -86,8 +93,15 @@ bool DevToolsLogReader::IsHeader(std::istringstream& header_stream) { ...@@ -86,8 +93,15 @@ bool DevToolsLogReader::IsHeader(std::istringstream& header_stream) {
return result; return result;
} }
void DevToolsLogReader::UndoGetNext(std::unique_ptr<LogEntry> next) {
peeked = std::move(next);
}
std::unique_ptr<LogEntry> DevToolsLogReader::GetNext( std::unique_ptr<LogEntry> DevToolsLogReader::GetNext(
LogEntry::Protocol protocol_type) { LogEntry::Protocol protocol_type) {
if (peeked) {
return std::move(peeked);
}
std::string next_line; std::string next_line;
while (true) { while (true) {
if (log_file.eof()) if (log_file.eof())
...@@ -103,8 +117,8 @@ std::unique_ptr<LogEntry> DevToolsLogReader::GetNext( ...@@ -103,8 +117,8 @@ std::unique_ptr<LogEntry> DevToolsLogReader::GetNext(
} }
if (log_entry->protocol_type != protocol_type) if (log_entry->protocol_type != protocol_type)
continue; continue;
if (!(log_entry->event_type == LogEntry::EventType::request && if (!(log_entry->event_type == LogEntry::kRequest &&
log_entry->protocol_type == LogEntry::Protocol::HTTP)) { log_entry->protocol_type == LogEntry::kHTTP)) {
log_entry->payload = GetJSONString(next_line_stream); log_entry->payload = GetJSONString(next_line_stream);
if (log_entry->payload == "") { if (log_entry->payload == "") {
LOG(ERROR) << "Problem parsing JSON from log file"; LOG(ERROR) << "Problem parsing JSON from log file";
...@@ -133,10 +147,10 @@ std::string DevToolsLogReader::GetJSONString( ...@@ -133,10 +147,10 @@ std::string DevToolsLogReader::GetJSONString(
closing_char = ']'; closing_char = ']';
break; break;
default: default:
return ""; return next_line; // For rare cases when payload is a string, not a JSON.
} }
while (true) { while (true) {
json += next_line; json += next_line + "\n";
opening_char_count += CountChar(next_line, opening_char, closing_char); opening_char_count += CountChar(next_line, opening_char, closing_char);
if (opening_char_count == 0) if (opening_char_count == 0)
break; break;
......
...@@ -28,6 +28,8 @@ ...@@ -28,6 +28,8 @@
// <id> is a sequential number to identify WebSocket commands with their // <id> is a sequential number to identify WebSocket commands with their
// responses // responses
// //
// <socket_id> identifies the WebSocket instance this entry came from, if any.
//
// <payload> is either the parameters in case of a WebSocket command, or the // <payload> is either the parameters in case of a WebSocket command, or the
// response in case of any response. It is always a JSON, and always spans // response in case of any response. It is always a JSON, and always spans
// multiple lines. // multiple lines.
...@@ -40,17 +42,18 @@ class LogEntry { ...@@ -40,17 +42,18 @@ class LogEntry {
~LogEntry(); ~LogEntry();
enum EventType { enum EventType {
request, // Command or Request depending on HTTP or WebSocket client kRequest, // Command or Request depending on HTTP or WebSocket client
response, kResponse,
event kEvent
}; };
enum Protocol { HTTP, WebSocket }; enum Protocol { kHTTP, kWebSocket };
EventType event_type; EventType event_type;
Protocol protocol_type; Protocol protocol_type;
std::string command_name; std::string command_name;
std::string payload; std::string payload;
int id; int id;
std::string socket_id;
bool error; bool error;
}; };
...@@ -67,8 +70,14 @@ class DevToolsLogReader { ...@@ -67,8 +70,14 @@ class DevToolsLogReader {
// no remaining entries of the specified type, or if there is some other // no remaining entries of the specified type, or if there is some other
// problem encountered like a truncated JSON, nullptr is returned. // problem encountered like a truncated JSON, nullptr is returned.
std::unique_ptr<LogEntry> GetNext(LogEntry::Protocol protocol_type); std::unique_ptr<LogEntry> GetNext(LogEntry::Protocol protocol_type);
// Undo the previous GetNext call.
//
// "Gives back" the unique_ptr to the LogEntry object to the log reader,
// to be returned the next time GetNext is called.
void UndoGetNext(std::unique_ptr<LogEntry> next);
private: private:
std::unique_ptr<LogEntry> peeked;
std::ifstream log_file; std::ifstream log_file;
// Starting with |header_line|, parse a JSON string out of the log file. // Starting with |header_line|, parse a JSON string out of the log file.
// //
...@@ -81,7 +90,7 @@ class DevToolsLogReader { ...@@ -81,7 +90,7 @@ class DevToolsLogReader {
// This only parses out the first two words of the line, meaning that the // This only parses out the first two words of the line, meaning that the
// stream can be re-used to parse the specifics of the entry after calling // stream can be re-used to parse the specifics of the entry after calling
// this. // this.
bool IsHeader(std::istringstream& line); bool IsHeader(std::istringstream& line) const;
// Count (number of opening_char) - (number of closing_char) in |line|. // Count (number of opening_char) - (number of closing_char) in |line|.
// //
......
...@@ -32,13 +32,13 @@ base::FilePath GetLogFileFromLiteral(const char literal[]) { ...@@ -32,13 +32,13 @@ base::FilePath GetLogFileFromLiteral(const char literal[]) {
TEST(DevToolsLogReaderTest, Basic) { TEST(DevToolsLogReaderTest, Basic) {
base::FilePath path = GetLogFileFromLiteral(kTestGetTitlePath); base::FilePath path = GetLogFileFromLiteral(kTestGetTitlePath);
DevToolsLogReader reader(path); DevToolsLogReader reader(path);
std::unique_ptr<LogEntry> next = reader.GetNext(LogEntry::Protocol::HTTP); std::unique_ptr<LogEntry> next = reader.GetNext(LogEntry::kHTTP);
EXPECT_TRUE(next != nullptr); EXPECT_TRUE(next != nullptr);
EXPECT_EQ(next->protocol_type, LogEntry::Protocol::HTTP); EXPECT_EQ(next->protocol_type, LogEntry::kHTTP);
EXPECT_EQ(next->command_name, "http://localhost:38845/json/version"); EXPECT_EQ(next->command_name, "http://localhost:38037/json/version");
next = reader.GetNext(LogEntry::Protocol::HTTP); next = reader.GetNext(LogEntry::kHTTP);
EXPECT_TRUE(next != nullptr); EXPECT_TRUE(next != nullptr);
EXPECT_EQ(next->payload, "{ \"string_key\": \"string_value\"}"); EXPECT_EQ(next->payload, "{\n \"string_key\": \"string_value\"\n}\n");
} }
TEST(DevToolsLogReaderTest, Multiple) { TEST(DevToolsLogReaderTest, Multiple) {
...@@ -46,33 +46,32 @@ TEST(DevToolsLogReaderTest, Multiple) { ...@@ -46,33 +46,32 @@ TEST(DevToolsLogReaderTest, Multiple) {
DevToolsLogReader reader(path); DevToolsLogReader reader(path);
std::unique_ptr<LogEntry> next; std::unique_ptr<LogEntry> next;
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
next = reader.GetNext(LogEntry::Protocol::HTTP); next = reader.GetNext(LogEntry::kHTTP);
EXPECT_TRUE(next != nullptr); EXPECT_TRUE(next != nullptr);
EXPECT_EQ(next->command_name, "http://localhost:38845/json"); EXPECT_EQ(next->command_name, "http://localhost:38037/json");
next = reader.GetNext(LogEntry::Protocol::HTTP); next = reader.GetNext(LogEntry::kHTTP);
EXPECT_EQ(next->payload, EXPECT_EQ(next->payload,
"[ { \"string_key1\": \"string_value1\"}, { " "[ {\n \"string_key1\": \"string_value1\"\n}, {\n "
"\"string_key2\": \"string_value2\"} ]"); "\"string_key2\": \"string_value2\"\n} ]\n");
} }
TEST(DevToolsLogReaderTest, EndOfFile) { TEST(DevToolsLogReaderTest, EndOfFile) {
base::FilePath path = GetLogFileFromLiteral(kOneEntryPath); base::FilePath path = GetLogFileFromLiteral(kOneEntryPath);
DevToolsLogReader reader(path); DevToolsLogReader reader(path);
std::unique_ptr<LogEntry> next = reader.GetNext(LogEntry::Protocol::HTTP); std::unique_ptr<LogEntry> next = reader.GetNext(LogEntry::kHTTP);
EXPECT_TRUE(next != nullptr); EXPECT_TRUE(next != nullptr);
next = reader.GetNext(LogEntry::Protocol::HTTP); next = reader.GetNext(LogEntry::kHTTP);
EXPECT_TRUE(next == nullptr); EXPECT_TRUE(next == nullptr);
} }
TEST(DevToolsLogReaderTest, WebSocketBasic) { TEST(DevToolsLogReaderTest, WebSocketBasic) {
base::FilePath path = GetLogFileFromLiteral(kTestGetTitlePath); base::FilePath path = GetLogFileFromLiteral(kTestGetTitlePath);
DevToolsLogReader reader(path); DevToolsLogReader reader(path);
std::unique_ptr<LogEntry> next = std::unique_ptr<LogEntry> next = reader.GetNext(LogEntry::kWebSocket);
reader.GetNext(LogEntry::Protocol::WebSocket);
EXPECT_TRUE(next != nullptr); EXPECT_TRUE(next != nullptr);
EXPECT_EQ(next->protocol_type, LogEntry::Protocol::WebSocket); EXPECT_EQ(next->protocol_type, LogEntry::kWebSocket);
EXPECT_EQ(next->event_type, LogEntry::EventType::request); EXPECT_EQ(next->event_type, LogEntry::kRequest);
EXPECT_EQ(next->command_name, "Log.enable"); EXPECT_EQ(next->command_name, "Log.enable");
EXPECT_EQ(next->id, 1); EXPECT_EQ(next->id, 1);
} }
...@@ -80,11 +79,10 @@ TEST(DevToolsLogReaderTest, WebSocketBasic) { ...@@ -80,11 +79,10 @@ TEST(DevToolsLogReaderTest, WebSocketBasic) {
TEST(DevToolsLogReaderTest, WebSocketMultiple) { TEST(DevToolsLogReaderTest, WebSocketMultiple) {
base::FilePath path = GetLogFileFromLiteral(kTestGetTitlePath); base::FilePath path = GetLogFileFromLiteral(kTestGetTitlePath);
DevToolsLogReader reader(path); DevToolsLogReader reader(path);
std::unique_ptr<LogEntry> next = std::unique_ptr<LogEntry> next = reader.GetNext(LogEntry::kWebSocket);
reader.GetNext(LogEntry::Protocol::WebSocket); next = reader.GetNext(LogEntry::kWebSocket);
next = reader.GetNext(LogEntry::Protocol::WebSocket);
EXPECT_TRUE(next != nullptr); EXPECT_TRUE(next != nullptr);
EXPECT_EQ(next->event_type, LogEntry::EventType::request); EXPECT_EQ(next->event_type, LogEntry::kRequest);
EXPECT_EQ(next->command_name, "DOM.getDocument"); EXPECT_EQ(next->command_name, "DOM.getDocument");
EXPECT_EQ(next->id, 2); EXPECT_EQ(next->id, 2);
} }
...@@ -94,18 +92,18 @@ TEST(DevToolsLogReaderTest, WebSocketPayload) { ...@@ -94,18 +92,18 @@ TEST(DevToolsLogReaderTest, WebSocketPayload) {
DevToolsLogReader reader(path); DevToolsLogReader reader(path);
std::unique_ptr<LogEntry> next; std::unique_ptr<LogEntry> next;
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
next = reader.GetNext(LogEntry::Protocol::WebSocket); next = reader.GetNext(LogEntry::kWebSocket);
EXPECT_TRUE(next != nullptr); EXPECT_TRUE(next != nullptr);
EXPECT_EQ(next->command_name, "Target.setAutoAttach"); EXPECT_EQ(next->command_name, "Target.setAutoAttach");
EXPECT_EQ(next->id, 3); EXPECT_EQ(next->id, 3);
EXPECT_EQ(next->payload, EXPECT_EQ(
"{ \"autoAttach\": true, \"waitForDebuggerOnStart\": false}"); next->payload,
"{\n \"autoAttach\": true,\n \"waitForDebuggerOnStart\": false\n}\n");
} }
TEST(DevToolsLogReaderTest, TruncatedJSON) { TEST(DevToolsLogReaderTest, TruncatedJSON) {
base::FilePath path = GetLogFileFromLiteral(kTruncatedJSONPath); base::FilePath path = GetLogFileFromLiteral(kTruncatedJSONPath);
DevToolsLogReader reader(path); DevToolsLogReader reader(path);
std::unique_ptr<LogEntry> next = std::unique_ptr<LogEntry> next = reader.GetNext(LogEntry::kWebSocket);
reader.GetNext(LogEntry::Protocol::WebSocket);
EXPECT_TRUE(next == nullptr); EXPECT_TRUE(next == nullptr);
} }
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/chromedriver/log_replay/log_replay_socket.h"
#include <memory>
#include "base/files/file_path.h"
#include "base/json/json_reader.h"
#include "base/values.h"
LogReplaySocket::LogReplaySocket(const base::FilePath& log_path)
: connected_(false), log_reader_(log_path) {}
LogReplaySocket::~LogReplaySocket() {}
void LogReplaySocket::SetId(const std::string& socket_id) {
socket_id_ = socket_id;
}
bool LogReplaySocket::IsConnected() {
return connected_;
}
bool LogReplaySocket::Connect(const GURL& url) {
connected_ = true;
return true;
}
bool LogReplaySocket::Send(const std::string& message) {
std::unique_ptr<base::Value> json = base::JSONReader::Read(message);
int id = json->FindKey("id")->GetInt();
max_id_ = id;
return true;
}
std::unique_ptr<LogEntry> LogReplaySocket::GetNextSocketEntry(
bool include_requests) {
while (true) {
std::unique_ptr<LogEntry> next = log_reader_.GetNext(LogEntry::kWebSocket);
if (next == nullptr)
return nullptr;
// wrong socket or it's a request (and |include_requests| is false)
if (next->socket_id != socket_id_ ||
(!include_requests && next->event_type == LogEntry::kRequest))
continue;
return next;
}
}
SyncWebSocket::StatusCode LogReplaySocket::ReceiveNextMessage(
std::string* message,
const Timeout& timeout) {
if (socket_id_ == "") {
return SyncWebSocket::kDisconnected;
}
std::unique_ptr<LogEntry> next = GetNextSocketEntry(false);
if (next->event_type == LogEntry::kResponse) {
// We have to build the messages back up to what they would have been
// in the actual WebSocket.
*message = "{\"id\":" + std::to_string(next->id) +
",\"result\":" + next->payload + "}";
return SyncWebSocket::kOk;
}
// it's an event
*message = "{\"method\":\"" + next->command_name +
"\",\"params\":" + next->payload + "}";
return SyncWebSocket::kOk;
}
// Ensures that we are not getting ahead of Chromedriver.
//
// This means not returning events or responses before ChromeDriver has taken
// the action that triggers them.
//
// There is a rare case where the following order of events occurs in the log:
// client-side command
// WebSocket Command (id=X) (resulting from client-side command)
// WebSocket Event
// WebSocket Response (id=X)
// To ensure that we don't fire the second event before the client-side
// command is called (thus probably causing an error), we must block when we
// see the WebSocket Command until that id is sent through the Send method.
// Such a WebSocket Command will always occur after a client-side command.
// If the event occurs between the client-side command and the WebSocket
// Command, it will be fine to fire at that time because ChromeDriver hasn't
// taken any action on the client-side command yet.
bool LogReplaySocket::HasNextMessage() {
// "Peek" the next entry
std::unique_ptr<LogEntry> next = GetNextSocketEntry(true);
if (next == nullptr)
return false;
if (next->event_type == LogEntry::kEvent) {
log_reader_.UndoGetNext(std::move(next));
return true;
}
bool haveMessage = next->id <= max_id_;
log_reader_.UndoGetNext(std::move(next));
return haveMessage;
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_TEST_CHROMEDRIVER_LOG_REPLAY_LOG_REPLAY_SOCKET_H_
#define CHROME_TEST_CHROMEDRIVER_LOG_REPLAY_LOG_REPLAY_SOCKET_H_
#include <string>
#include "chrome/test/chromedriver/log_replay/devtools_log_reader.h"
#include "chrome/test/chromedriver/net/sync_websocket.h"
// A "WebSocket" class for getting commands from a log file.
//
// Instead of communicating with DevTools, this implementation looks up the
// intended results of commands in a ChromeDriver log file. This enables the
// DevTools-side commands and responses to be replayed from a past session.
class LogReplaySocket : public SyncWebSocket {
public:
// Initialize a LogReplaySocket with a ChromeDriver log.
explicit LogReplaySocket(const base::FilePath& log_path);
~LogReplaySocket() override;
// Set the ID of this instance. This is intended to be the id_ of the
// DevToolsClientImpl instance that owns this LogReplaySocket, which is
// an identifier for the renderer process that this DevTools instance
// was talking to. Since ChromeDriver logs often include messages from
// multiple renderers, this enables a LogReplaySocket to know which
// of these messages it should observe. Note that the id_ corresponds
// to a frameID or window ID, so it stays stable between the old and
// replay sessions (this is in contrast to sessionIDs, which change).
void SetId(const std::string& socket_id) override;
// The Connection methods here basically just mock out behavior of a
// real SyncWebSocket
bool IsConnected() override;
bool Connect(const GURL& url) override;
// "Sends" a message to the "renderer". This doesn't really give us any
// actions to do (since we would know how to respond even if the message
// wasn't sent). However, we need to keep track of the sequential id of
// the latest message because we don't want to return the responses for
// messages that have not been sent yet.
bool Send(const std::string& message) override;
// Return the next message (event or response) in the |message| parameter.
// timeout is ignored (since the response is either ready or there is
// some kind of problem with the log).
StatusCode ReceiveNextMessage(std::string* message,
const Timeout& timeout) override;
// Returns true if we "should" have the next message - that is, the next
// message in the log either is an event or a response for a message that
// has been sent.
bool HasNextMessage() override;
private:
// Return the next WebSocket entry in the log. Returns requests only if
// |include_requests| is true
std::unique_ptr<LogEntry> GetNextSocketEntry(bool include_requests);
bool connected_;
int max_id_;
DevToolsLogReader log_reader_;
std::string socket_id_;
};
#endif // CHROME_TEST_CHROMEDRIVER_LOG_REPLAY_LOG_REPLAY_SOCKET_H_
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/chromedriver/log_replay/replay_http_client.h"
#include <utility>
#include "chrome/test/chromedriver/chrome/device_metrics.h"
#include "chrome/test/chromedriver/net/url_request_context_getter.h"
#include "url/gurl.h"
namespace {
// Fetch the path from the given url (i.e. "http://foo.bar/baz" -> "/baz")
std::string UrlPath(const std::string& url) {
GURL gurl(url);
return gurl.path();
}
} // namespace
ReplayHttpClient::ReplayHttpClient(
const NetAddress& address,
scoped_refptr<URLRequestContextGetter> context_getter,
const SyncWebSocketFactory& socket_factory,
std::unique_ptr<DeviceMetrics> device_metrics,
std::unique_ptr<std::set<WebViewInfo::Type>> window_types,
std::string page_load_strategy,
const base::FilePath& log_path)
: DevToolsHttpClient(address,
context_getter,
socket_factory,
std::move(device_metrics),
std::move(window_types),
page_load_strategy),
log_reader_(log_path) {}
ReplayHttpClient::~ReplayHttpClient() {}
bool ReplayHttpClient::FetchUrlAndLog(const std::string& url,
URLRequestContextGetter* getter,
std::string* response) {
VLOG(1) << "DevTools HTTP Request: " << url;
std::string path_from_url = UrlPath(url);
std::unique_ptr<LogEntry> next_command = log_reader_.GetNext(LogEntry::kHTTP);
// The HTTP requests should happen in the same order as they occur in the
// log file. We use this as a sanity check and return false if something
// appears to be out of order or if the log file appears truncated.
if (next_command == nullptr) {
return false;
}
if (UrlPath(next_command->command_name) == path_from_url &&
next_command->event_type == LogEntry::kRequest) {
std::unique_ptr<LogEntry> next_response =
log_reader_.GetNext(LogEntry::kHTTP);
if (next_response == nullptr) {
return false;
}
*response = next_response->payload;
VLOG(1) << "DevTools HTTP Response: " << next_response->payload;
return true;
}
return false;
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_TEST_CHROMEDRIVER_LOG_REPLAY_REPLAY_HTTP_CLIENT_H_
#define CHROME_TEST_CHROMEDRIVER_LOG_REPLAY_REPLAY_HTTP_CLIENT_H_
#include <memory>
#include <set>
#include <string>
#include "base/files/file_path.h"
#include "chrome/test/chromedriver/chrome/browser_info.h"
#include "chrome/test/chromedriver/chrome/devtools_http_client.h"
#include "chrome/test/chromedriver/log_replay/devtools_log_reader.h"
#include "chrome/test/chromedriver/net/sync_websocket_factory.h"
// Subclass of DevToolsHttpClient that redirects communication
// that would happen with Chrome to a DevToolsLogReader (i.e. a ChromeDriver
// log file). This enables log replay of DevTools HTTP communication with
// DevTools.
class ReplayHttpClient : public DevToolsHttpClient {
public:
// Initializes a DevToolsLogReader with the given log file.
ReplayHttpClient(const NetAddress& address,
scoped_refptr<URLRequestContextGetter> context_getter,
const SyncWebSocketFactory& socket_factory,
std::unique_ptr<DeviceMetrics> device_metrics,
std::unique_ptr<std::set<WebViewInfo::Type>> window_types,
std::string page_load_strategy,
const base::FilePath& log_file);
~ReplayHttpClient() override;
private:
// DevToolsLogReader that we read the responses from
DevToolsLogReader log_reader_;
// This is the only function that we override from DevToolsHttpClient;
// instead of actually sending an HTTP request it looks for the
// corresponding request in the log file and returns the response accordingly.
bool FetchUrlAndLog(const std::string& url,
URLRequestContextGetter* getter,
std::string* response) override;
};
#endif // CHROME_TEST_CHROMEDRIVER_LOG_REPLAY_REPLAY_HTTP_CLIENT_H_
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
[1533082321.495][DEBUG]: DevTools WebSocket Command: Target.setAutoAttach (id=3) { [1533082321.495][DEBUG]: DevTools WebSocket Command Target.setAutoAttach (id=3) 09BD0B99D6BC582B6BEC7F53F420E62B {
"autoAttach": true, "autoAttach": true,
"waitForDebuggerOnStart": false, "waitForDebuggerOnStart": false,
\ No newline at end of file
...@@ -4,4 +4,8 @@ include_rules = [ ...@@ -4,4 +4,8 @@ include_rules = [
"+chrome/test/chromedriver/chrome/status.h", "+chrome/test/chromedriver/chrome/status.h",
"+chrome/test/chromedriver/net", "+chrome/test/chromedriver/net",
# LogReplaySocket is a mock out of a chromedriver/net impelementation
# that logically belongs in chromedriver/log_replay but must be included
# for sync_websocket_factory.cc
"+chrome/test/chromedriver/log_replay/log_replay_socket.h",
] ]
...@@ -21,6 +21,8 @@ class SyncWebSocket { ...@@ -21,6 +21,8 @@ class SyncWebSocket {
virtual ~SyncWebSocket() {} virtual ~SyncWebSocket() {}
virtual void SetId(const std::string& socket_id) {}
// Return true if connected, otherwise return false. // Return true if connected, otherwise return false.
virtual bool IsConnected() = 0; virtual bool IsConnected() = 0;
......
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
#include "chrome/test/chromedriver/net/sync_websocket_factory.h" #include "chrome/test/chromedriver/net/sync_websocket_factory.h"
#include "base/bind.h" #include "base/bind.h"
#include "base/command_line.h"
#include "base/memory/ref_counted.h" #include "base/memory/ref_counted.h"
#include "chrome/test/chromedriver/log_replay/log_replay_socket.h"
#include "chrome/test/chromedriver/net/sync_websocket_impl.h" #include "chrome/test/chromedriver/net/sync_websocket_impl.h"
#include "chrome/test/chromedriver/net/url_request_context_getter.h" #include "chrome/test/chromedriver/net/url_request_context_getter.h"
...@@ -17,9 +19,21 @@ std::unique_ptr<SyncWebSocket> CreateSyncWebSocket( ...@@ -17,9 +19,21 @@ std::unique_ptr<SyncWebSocket> CreateSyncWebSocket(
new SyncWebSocketImpl(context_getter.get())); new SyncWebSocketImpl(context_getter.get()));
} }
std::unique_ptr<SyncWebSocket> CreateReplayWebSocket(base::FilePath log_path) {
return std::unique_ptr<LogReplaySocket>(new LogReplaySocket(log_path));
}
} // namespace } // namespace
SyncWebSocketFactory CreateSyncWebSocketFactory( SyncWebSocketFactory CreateSyncWebSocketFactory(
URLRequestContextGetter* getter) { URLRequestContextGetter* getter) {
return base::Bind(&CreateSyncWebSocket, base::WrapRefCounted(getter)); const base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
if (cmd_line->HasSwitch("devtools-replay")) {
base::CommandLine::StringType log_path_str =
cmd_line->GetSwitchValueNative("devtools-replay");
base::FilePath log_path(log_path_str);
return base::BindRepeating(&CreateReplayWebSocket, log_path);
}
return base::BindRepeating(&CreateSyncWebSocket,
base::WrapRefCounted(getter));
} }
...@@ -33,6 +33,7 @@ class SyncWebSocketImpl : public SyncWebSocket { ...@@ -33,6 +33,7 @@ class SyncWebSocketImpl : public SyncWebSocket {
explicit SyncWebSocketImpl(net::URLRequestContextGetter* context_getter); explicit SyncWebSocketImpl(net::URLRequestContextGetter* context_getter);
~SyncWebSocketImpl() override; ~SyncWebSocketImpl() override;
void SetId(const std::string& socket_id) override {}
// Overridden from SyncWebSocket: // Overridden from SyncWebSocket:
bool IsConnected() override; bool IsConnected() override;
bool Connect(const GURL& url) override; bool Connect(const GURL& url) override;
......
...@@ -13,14 +13,18 @@ import urllib2 ...@@ -13,14 +13,18 @@ import urllib2
class Server(object): class Server(object):
"""A running ChromeDriver server.""" """A running ChromeDriver server."""
def __init__(self, exe_path, log_path=None, verbose=True, replayable=False): def __init__(self, exe_path, log_path=None, verbose=True,
replayable=False, devtools_replay_path=None):
"""Starts the ChromeDriver server and waits for it to be ready. """Starts the ChromeDriver server and waits for it to be ready.
Args: Args:
exe_path: path to the ChromeDriver executable exe_path: path to the ChromeDriver executable
log_path: path to the log file log_path: path to the log file
verbose: make the logged data verbose
replayable: don't truncate strings in log to make the session replayable
devtools_replay_path: replay devtools events from the log at this path
Raises: Raises:
RuntimeError if ChromeDriver fails to start RuntimeError: if ChromeDriver fails to start
""" """
if not os.path.exists(exe_path): if not os.path.exists(exe_path):
raise RuntimeError('ChromeDriver exe not found at: ' + exe_path) raise RuntimeError('ChromeDriver exe not found at: ' + exe_path)
...@@ -28,12 +32,15 @@ class Server(object): ...@@ -28,12 +32,15 @@ class Server(object):
port = self._FindOpenPort() port = self._FindOpenPort()
chromedriver_args = [exe_path, '--port=%d' % port] chromedriver_args = [exe_path, '--port=%d' % port]
if log_path: if log_path:
chromedriver_args.extend(['--log-path=%s' %log_path]) chromedriver_args.extend(['--log-path=%s' % log_path])
if verbose: if verbose:
chromedriver_args.extend(['--verbose', chromedriver_args.extend(['--verbose',
'--vmodule=*/chrome/test/chromedriver/*=3']) '--vmodule=*/chrome/test/chromedriver/*=3'])
if replayable: if replayable:
chromedriver_args.extend(['--replayable']); chromedriver_args.extend(['--replayable'])
if devtools_replay_path:
chromedriver_args.extend(['--devtools-replay=%s' % devtools_replay_path])
self._process = subprocess.Popen(chromedriver_args) self._process = subprocess.Popen(chromedriver_args)
self._url = 'http://127.0.0.1:%d' % port self._url = 'http://127.0.0.1:%d' % port
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment