blob: 1071cd5ba88a99214f82b6d99bb58883a7621ec0 [file]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/base/devtools_listener.h"
#include <stddef.h>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/hash/md5.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "url/url_util.h"
namespace coverage {
namespace {
base::StringPiece SpanToStringPiece(const base::span<const uint8_t>& s) {
return {reinterpret_cast<const char*>(s.data()), s.size()};
}
std::string EncodeURIComponent(const std::string& component) {
url::RawCanonOutputT<char> encoded;
url::EncodeURIComponent(component, &encoded);
return std::string(encoded.view());
}
} // namespace
DevToolsListener::DevToolsListener(content::DevToolsAgentHost* host,
uint32_t uuid)
: uuid_(base::StringPrintf("%u", uuid)) {
CHECK(!host->IsAttached());
host->AttachClient(this);
Start(host);
}
DevToolsListener::~DevToolsListener() = default;
void DevToolsListener::Navigated(content::DevToolsAgentHost* host) {
CHECK(host->IsAttached() && attached_);
navigated_ = StartJSCoverage(host);
}
bool DevToolsListener::HasCoverage(content::DevToolsAgentHost* host) {
return attached_ && navigated_;
}
void DevToolsListener::GetCoverage(content::DevToolsAgentHost* host,
const base::FilePath& store,
const std::string& test) {
if (HasCoverage(host))
StopAndStoreJSCoverage(host, store, test);
navigated_ = false;
}
void DevToolsListener::Detach(content::DevToolsAgentHost* host) {
if (attached_)
host->DetachClient(this);
navigated_ = false;
attached_ = false;
}
std::string DevToolsListener::HostString(content::DevToolsAgentHost* host,
const std::string& prefix) {
std::string result = base::StrCat(
{prefix, " ", host->GetType(), " title: ", host->GetTitle()});
std::string description = host->GetDescription();
if (!description.empty())
base::StrAppend(&result, {" description: ", description});
std::string url = host->GetURL().spec();
if (!url.empty())
base::StrAppend(&result, {" URL: ", url});
return result;
}
void DevToolsListener::SetupCoverageStore(const base::FilePath& store_path) {
if (!base::PathExists(store_path))
CHECK(base::CreateDirectory(store_path));
base::FilePath tests = store_path.AppendASCII("tests");
if (!base::PathExists(tests))
CHECK(base::CreateDirectory(tests));
base::FilePath scripts = store_path.AppendASCII("scripts");
if (!base::PathExists(scripts))
CHECK(base::CreateDirectory(scripts));
}
void DevToolsListener::Start(content::DevToolsAgentHost* host) {
std::string enable_runtime = "{\"id\":10,\"method\":\"Runtime.enable\"}";
SendCommandMessage(host, enable_runtime);
std::string enable_page = "{\"id\":11,\"method\":\"Page.enable\"}";
SendCommandMessage(host, enable_page);
}
bool DevToolsListener::StartJSCoverage(content::DevToolsAgentHost* host) {
std::string enable_profiler = "{\"id\":20,\"method\":\"Profiler.enable\"}";
SendCommandMessage(host, enable_profiler);
std::string start_precise_coverage =
"{\"id\":21,\"method\":\"Profiler.startPreciseCoverage\",\"params\":{"
"\"callCount\":true,\"detailed\":true}}";
SendCommandMessage(host, start_precise_coverage);
std::string enable_debugger = "{\"id\":22,\"method\":\"Debugger.enable\"}";
SendCommandMessage(host, enable_debugger);
std::string skip_all_pauses =
"{\"id\":23,\"method\":\"Debugger.setSkipAllPauses\""
",\"params\":{\"skip\":true}}";
SendCommandMessage(host, skip_all_pauses);
return true;
}
void DevToolsListener::StopAndStoreJSCoverage(content::DevToolsAgentHost* host,
const base::FilePath& store,
const std::string& test) {
std::string get_precise_coverage =
"{\"id\":40,\"method\":\"Profiler.takePreciseCoverage\"}";
SendCommandMessage(host, get_precise_coverage);
if (!AwaitCommandResponse(40)) {
LOG(ERROR) << "Host has been destroyed whilst getting precise coverage";
return;
}
script_coverage_ = std::move(value_);
base::Value::Dict* result = script_coverage_.FindDict("result");
CHECK(result) << "result key is null: " << script_coverage_;
base::Value::List* coverage_entries = result->FindList("result");
CHECK(coverage_entries) << "Can't find result key: " << *result;
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
VerifyAllScriptsAreParsedRepeatedly(coverage_entries, run_loop.QuitClosure(),
/*retries=*/10);
run_loop.Run();
CHECK(all_scripts_parsed_) << "All scripts in coverage results were not "
"retrieved after 10s of waiting";
StoreScripts(host, store);
std::string stop_debugger = "{\"id\":41,\"method\":\"Debugger.disable\"}";
SendCommandMessage(host, stop_debugger);
std::string stop_profiler = "{\"id\":42,\"method\":\"Profiler.disable\"}";
SendCommandMessage(host, stop_profiler);
base::Value::List entries;
for (base::Value& entry_value : *coverage_entries) {
CHECK(entry_value.is_dict()) << "Entry is not dictionary: " << entry_value;
base::Value::Dict& entry = entry_value.GetDict();
std::string* script_id = entry.FindString("scriptId");
CHECK(script_id) << "Can't find scriptId: " << entry;
const auto it = script_id_map_.find(*script_id);
if (it == script_id_map_.end())
continue;
entry.Set("hash", it->second);
entries.Append(entry.Clone());
}
std::string url = host->GetURL().spec();
result->Set("encodedHostURL", EncodeURIComponent(url));
result->Set("hostTitle", host->GetTitle());
result->Set("hostType", host->GetType());
result->Set("hostTest", test);
result->Set("hostURL", url);
std::string md5 = base::MD5String(HostString(host, test));
std::string coverage = base::StrCat({test, ".", md5, uuid_, ".cov.json"});
base::FilePath path = store.AppendASCII("tests").AppendASCII(coverage);
result->Set("result", std::move(entries));
CHECK(base::JSONWriter::Write(*result, &coverage));
base::WriteFile(path, coverage);
script_coverage_.clear();
script_hash_map_.clear();
script_id_map_.clear();
scripts_.clear();
LOG_IF(ERROR, !AwaitCommandResponse(42))
<< "Host has been destroyed whilst waiting, coverage coverage already "
"extracted though";
value_.clear();
all_scripts_parsed_ = false;
}
void DevToolsListener::VerifyAllScriptsAreParsedRepeatedly(
const base::Value::List* coverage_entries,
base::OnceClosure done_callback,
int retries) {
CHECK_GT(retries, 0);
CHECK(done_callback);
// Collect all the scriptId's that have been seen via the aggregated
// `Debugger.scriptParsed` events.
std::set<std::string> script_ids;
for (base::Value::Dict& script : scripts_) {
std::string* id = script.FindStringByDottedPath("params.scriptId");
if (!id) {
continue;
}
script_ids.emplace(*id);
}
// All the scriptId values seen in the coverage values must have been sent via
// the `Debugger.scriptParsed` event. This tries 10 times with a 1 second
// pause in between verification attempts.
bool missing_script = false;
for (const auto& entry : *coverage_entries) {
const std::string* id = entry.GetDict().FindString("scriptId");
CHECK(id) << "Can't extract scriptId: " << entry;
if (!script_ids.contains(*id)) {
missing_script = true;
break;
}
}
all_scripts_parsed_ = !missing_script;
if (all_scripts_parsed_ || --retries == 0) {
std::move(done_callback).Run();
return;
}
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&DevToolsListener::VerifyAllScriptsAreParsedRepeatedly,
weak_ptr_factory_.GetWeakPtr(), coverage_entries,
std::move(done_callback), retries),
base::Seconds(1));
}
void DevToolsListener::StoreScripts(content::DevToolsAgentHost* host,
const base::FilePath& store) {
for (base::Value::Dict& script : scripts_) {
std::string id;
{
std::string* id_ptr = script.FindStringByDottedPath("params.scriptId");
CHECK(id_ptr);
CHECK(!id_ptr->empty());
id = *id_ptr;
}
std::string url;
{
std::string* url_ptr = script.FindStringByDottedPath("params.url");
if (!url_ptr)
url_ptr = script.FindStringByDottedPath("params.sourceURL");
if (!url_ptr || url_ptr->empty()) {
value_.clear();
continue;
}
url = *url_ptr;
}
std::string get_script_source = base::StringPrintf(
"{\"id\":50,\"method\":\"Debugger.getScriptSource\""
",\"params\":{\"scriptId\":\"%s\"}}",
id.c_str());
SendCommandMessage(host, get_script_source);
if (!AwaitCommandResponse(50)) {
LOG(ERROR) << "Host has been destroyed whilst getting script source, "
"skipping remaining script sources";
return;
}
std::string text;
{
base::Value::Dict* result = value_.FindDict("result");
// TODO(crbug/1206082): In some cases the v8 isolate may clear out the
// script source during execution. This can lead to the Debugger seeing a
// scriptId during execution but when it comes time to retrieving the
// source can no longer find the ID. For now we simply ignore these, but
// we need to find a better way to handle this.
if (!result) {
LOG(ERROR) << "Can't find result from Debugger.getScriptSource: "
<< value_;
return;
}
std::string* text_ptr = result->FindString("scriptSource");
if (!text_ptr || text_ptr->empty()) {
value_.clear();
continue;
}
text = *text_ptr;
}
std::string hash;
{
std::string* hash_ptr = script.FindStringByDottedPath("params.hash");
CHECK(hash_ptr);
hash = *hash_ptr;
}
if (script_id_map_.find(id) != script_id_map_.end())
LOG(FATAL) << "Duplicate script by id " << url;
script_id_map_[id] = hash;
CHECK(!hash.empty());
if (script_hash_map_.find(hash) != script_hash_map_.end()) {
value_.clear();
continue;
}
script_hash_map_[hash] = id;
base::Value::Dict* params = script.FindDict("params");
CHECK(params) << "Can't find params from script: " << script;
params->Set("encodedURL", EncodeURIComponent(url));
params->Set("hash", hash);
params->Set("text", text);
params->Set("url", url);
base::FilePath path =
store.AppendASCII("scripts").AppendASCII(hash.append(".js.json"));
CHECK(base::JSONWriter::Write(*params, &text));
if (!base::PathExists(path)) // script de-duplication
base::WriteFile(path, text);
value_.clear();
}
}
void DevToolsListener::SendCommandMessage(content::DevToolsAgentHost* host,
const std::string& command) {
auto message = base::as_bytes(base::make_span(command));
host->DispatchProtocolMessage(this, message);
}
bool DevToolsListener::AwaitCommandResponse(int id) {
if (!attached_ && !navigated_) {
return false;
}
value_.clear();
value_id_ = id;
base::RunLoop run_loop;
value_closure_ = run_loop.QuitClosure();
run_loop.Run();
return attached_ && navigated_;
}
void DevToolsListener::DispatchProtocolMessage(
content::DevToolsAgentHost* host,
base::span<const uint8_t> message) {
if (!navigated_)
return;
if (VLOG_IS_ON(2))
VLOG(2) << SpanToStringPiece(message);
std::optional<base::Value> value =
base::JSONReader::Read(SpanToStringPiece(message));
CHECK(value.has_value()) << "Cannot parse as JSON: "
<< SpanToStringPiece(message);
base::Value::Dict dict_value = std::move(value.value().GetDict());
std::string* method = dict_value.FindString("method");
if (method) {
if (*method == "Runtime.executionContextsCreated") {
scripts_.clear();
} else if (*method == "Debugger.scriptParsed" && !all_scripts_parsed_) {
scripts_.push_back(std::move(dict_value));
}
return;
}
std::optional<int> id = dict_value.FindInt("id");
if (id.has_value() && id.value() == value_id_) {
value_ = std::move(dict_value);
CHECK(value_closure_);
std::move(value_closure_).Run();
}
}
bool DevToolsListener::MayAttachToURL(const GURL& url, bool is_webui) {
return true;
}
void DevToolsListener::AgentHostClosed(content::DevToolsAgentHost* host) {
navigated_ = false;
attached_ = false;
if (value_closure_) {
std::move(value_closure_).Run();
}
}
} // namespace coverage