Commit e841cf3f authored by Ben Reich's avatar Ben Reich Committed by Commit Bot

[Files App] Adding DevToolsListener class

Add a class that emits the minimal set of messages to collect JS code
coverage via Chrome Devtools Protocol from a WebContents host.

Bug: 1113941
Change-Id: I3119f4417cf3b49a16428888126c3bfa639472c9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2347265Reviewed-by: default avatarNoel Gordon <noel@chromium.org>
Commit-Queue: Noel Gordon <noel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#813115}
parent 9f41cd63
// Copyright 2020 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/browser/chromeos/file_manager/browser_test_devtools_listener.h"
#include <stddef.h>
#include <map>
#include <memory>
#include <string>
#include <vector>
#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/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "url/url_util.h"
namespace file_manager {
namespace {
base::span<const uint8_t> StringToSpan(const std::string& str) {
return base::as_bytes(base::make_span(str));
}
std::string EncodedURL(const std::string& url) {
url::RawCanonOutputT<char> canonical_url;
url::EncodeURIComponent(url.c_str(), url.size(), &canonical_url);
return std::string(canonical_url.data(), canonical_url.length());
}
} // 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::Start(content::DevToolsAgentHost* host) {
std::string enable_runtime = "{\"id\":10,\"method\":\"Runtime.enable\"}";
host->DispatchProtocolMessage(this, StringToSpan(enable_runtime));
std::string enable_page = "{\"id\":11,\"method\":\"Page.enable\"}";
host->DispatchProtocolMessage(this, StringToSpan(enable_page));
}
bool DevToolsListener::StartJSCoverage(content::DevToolsAgentHost* host) {
std::string enable_profiler = "{\"id\":20,\"method\":\"Profiler.enable\"}";
host->DispatchProtocolMessage(this, StringToSpan(enable_profiler));
std::string start_precise_coverage =
"{\"id\":21,\"method\":\"Profiler.startPreciseCoverage\",\"params\":{"
"\"callCount\":false,\"detailed\":true}}";
host->DispatchProtocolMessage(this, StringToSpan(start_precise_coverage));
std::string enable_debugger = "{\"id\":22,\"method\":\"Debugger.enable\"}";
host->DispatchProtocolMessage(this, StringToSpan(enable_debugger));
std::string skip_pauses =
"{\"id\":23,\"method\":\"Debugger.setSkipAllPauses\""
",\"params\":{\"skip\":true}}";
host->DispatchProtocolMessage(this, StringToSpan(skip_pauses));
return true;
}
void DevToolsListener::StopAndStoreJSCoverage(content::DevToolsAgentHost* host,
const base::FilePath& store,
const std::string test) {
std::string precise_coverage =
"{\"id\":40,\"method\":\"Profiler.takePreciseCoverage\"}";
host->DispatchProtocolMessage(this, StringToSpan(precise_coverage));
AwaitMessageResponse(40);
script_coverage_.reset(value_.release());
StoreScripts(host, store);
std::string debugger = "{\"id\":41,\"method\":\"Debugger.disable\"}";
host->DispatchProtocolMessage(this, StringToSpan(debugger));
std::string profiler = "{\"id\":42,\"method\":\"Profiler.disable\"}";
host->DispatchProtocolMessage(this, StringToSpan(profiler));
base::DictionaryValue* result = nullptr;
CHECK(script_coverage_->GetDictionary("result", &result));
base::ListValue* coverage_entries = nullptr;
CHECK(result->GetList("result", &coverage_entries));
auto entries = std::make_unique<base::ListValue>();
for (size_t i = 0; i != coverage_entries->GetSize(); ++i) {
base::DictionaryValue* entry = nullptr;
CHECK(coverage_entries->GetDictionary(i, &entry));
std::string script_id;
CHECK(entry->GetString("scriptId", &script_id));
const auto it = script_id_map_.find(script_id);
if (it == script_id_map_.end())
continue;
CHECK(entry->SetString("hash", it->second));
entries->Append(entry->CreateDeepCopy());
}
const std::string url = host->GetURL().spec();
CHECK(result->SetString("encodedHostURL", EncodedURL(url)));
CHECK(result->SetString("hostTitle", host->GetTitle()));
CHECK(result->SetString("hostType", host->GetType()));
CHECK(result->SetString("hostTest", test));
CHECK(result->SetString("hostURL", url));
const std::string md5 = base::MD5String(HostString(host, test));
std::string coverage = base::StrCat({test, ".", md5, uuid_, ".js.json"});
base::FilePath path = store.AppendASCII("tests").Append(coverage);
CHECK(result->SetList("result", std::move(entries)));
CHECK(base::JSONWriter::Write(*result, &coverage));
base::WriteFile(path, coverage.data(), coverage.size());
script_coverage_.reset();
script_hash_map_.clear();
script_id_map_.clear();
script_.clear();
AwaitMessageResponse(42);
value_.reset();
}
void DevToolsListener::StoreScripts(content::DevToolsAgentHost* host,
const base::FilePath& store) {
for (size_t i = 0; i < script_.size(); ++i, value_.reset()) {
std::string id;
CHECK(script_[i]->GetString("params.scriptId", &id));
CHECK(!id.empty());
std::string url;
if (!script_[i]->GetString("params.url", &url))
script_[i]->GetString("params.sourceURL", &url);
if (url.empty())
continue;
std::string script_source = base::StringPrintf(
"{\"id\":50,\"method\":\"Debugger.getScriptSource\""
",\"params\":{\"scriptId\":\"%s\"}}",
id.c_str());
host->DispatchProtocolMessage(this, StringToSpan(script_source));
AwaitMessageResponse(50);
base::DictionaryValue* result = nullptr;
CHECK(value_->GetDictionary("result", &result));
std::string text;
result->GetString("scriptSource", &text);
if (text.empty())
continue;
std::string hash;
CHECK(script_[i]->GetString("params.hash", &hash));
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())
continue;
script_hash_map_[hash] = id;
base::DictionaryValue* script = nullptr;
CHECK(script_[i]->GetDictionary("params", &script));
CHECK(script->SetString("encodedURL", EncodedURL(url)));
CHECK(script->SetString("hash", hash));
CHECK(script->SetString("text", text));
CHECK(script->SetString("url", url));
base::FilePath path = store.AppendASCII(hash.append(".js.json"));
CHECK(base::JSONWriter::Write(*script, &text));
if (!base::PathExists(path)) // Deduplication
base::WriteFile(path, text.data(), text.size());
}
}
void DevToolsListener::AwaitMessageResponse(int id) {
value_.reset();
value_id_ = id;
base::RunLoop run_loop;
value_closure_ = run_loop.QuitClosure();
run_loop.Run();
}
void DevToolsListener::DispatchProtocolMessage(
content::DevToolsAgentHost* host,
base::span<const uint8_t> span_message) {
if (!navigated_)
return;
std::string message(reinterpret_cast<const char*>(span_message.data()),
span_message.size());
std::unique_ptr<base::DictionaryValue> response =
base::DictionaryValue::From(base::JSONReader::ReadDeprecated(message));
CHECK(response);
std::string* method = response->FindStringPath("method");
if (method) {
if (*method == "Debugger.scriptParsed")
script_.push_back(std::move(response));
else if (*method == "Runtime.executionContextsCreated")
script_.clear();
return;
}
base::Optional<int> id = response->FindIntPath("id");
if (id.has_value() && id.value() == value_id_) {
value_.reset(response.release());
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) {
CHECK(!value_closure_);
navigated_ = false;
attached_ = false;
}
} // namespace file_manager
// Copyright 2020 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_BROWSER_CHROMEOS_FILE_MANAGER_BROWSER_TEST_DEVTOOLS_LISTENER_H_
#define CHROME_BROWSER_CHROMEOS_FILE_MANAGER_BROWSER_TEST_DEVTOOLS_LISTENER_H_
#include <map>
#include <memory>
#include <string>
#include <vector>
#include "base/values.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/devtools_agent_host_client.h"
namespace file_manager {
// Collects code coverage from a WebContents during a
// browser test using Chrome Devtools Protocol (CDP).
class DevToolsListener : public content::DevToolsAgentHostClient {
public:
// Attaches to a host and enables CDP.
DevToolsListener(content::DevToolsAgentHost* host, uint32_t uuid);
~DevToolsListener() override;
// Starts code coverage.
void Navigated(content::DevToolsAgentHost* host);
// Returns true if host has started code coverage.
bool HasCoverage(content::DevToolsAgentHost* host);
// If host HasCoverage() collect the coverage and
// write it into the |store|.
void GetCoverage(content::DevToolsAgentHost* host,
const base::FilePath& store,
const std::string test);
// Detaches from a host.
void Detach(content::DevToolsAgentHost* host);
// Returns a string that uniquely identifies a host
// with an optional prefix.
static std::string HostString(content::DevToolsAgentHost* host,
const std::string prefix);
private:
// Enable CDP on host.
void Start(content::DevToolsAgentHost* host);
// Starts JavaScript code coverage on host.
bool StartJSCoverage(content::DevToolsAgentHost* host);
// Collects JavaScript code coverage on host and writes
// it into the |store|.
void StopAndStoreJSCoverage(content::DevToolsAgentHost* host,
const base::FilePath& store,
const std::string test);
// Stores scripts that are parsed during execution on host.
void StoreScripts(content::DevToolsAgentHost* host,
const base::FilePath& store);
// Await CDP response to command |id|.
void AwaitMessageResponse(int id);
// Receives CDP messages sent by host.
void DispatchProtocolMessage(content::DevToolsAgentHost* host,
base::span<const uint8_t> span_message) override;
// Returns true if URL should be attached to.
bool MayAttachToURL(const GURL& url, bool is_webui) override;
// Clean up when host is closed.
void AgentHostClosed(content::DevToolsAgentHost* host) override;
private:
std::vector<std::unique_ptr<base::DictionaryValue>> script_;
std::unique_ptr<base::DictionaryValue> script_coverage_;
std::map<std::string, std::string> script_hash_map_;
std::map<std::string, std::string> script_id_map_;
base::OnceClosure value_closure_;
std::unique_ptr<base::DictionaryValue> value_;
int value_id_;
const std::string uuid_;
bool navigated_ = false;
bool attached_ = true;
};
} // namespace file_manager
#endif // CHROME_BROWSER_CHROMEOS_FILE_MANAGER_BROWSER_TEST_DEVTOOLS_LISTENER_H_
...@@ -2374,6 +2374,8 @@ if (!is_android) { ...@@ -2374,6 +2374,8 @@ if (!is_android) {
"../browser/chromeos/file_manager/file_manager_browsertest.cc", "../browser/chromeos/file_manager/file_manager_browsertest.cc",
"../browser/chromeos/file_manager/file_manager_browsertest_base.cc", "../browser/chromeos/file_manager/file_manager_browsertest_base.cc",
"../browser/chromeos/file_manager/file_manager_browsertest_base.h", "../browser/chromeos/file_manager/file_manager_browsertest_base.h",
"../browser/chromeos/file_manager/browser_test_devtools_listener.cc",
"../browser/chromeos/file_manager/browser_test_devtools_listener.h",
"../browser/chromeos/file_manager/file_manager_jstest.cc", "../browser/chromeos/file_manager/file_manager_jstest.cc",
"../browser/chromeos/file_manager/file_manager_jstest_base.cc", "../browser/chromeos/file_manager/file_manager_jstest_base.cc",
"../browser/chromeos/file_manager/file_manager_jstest_base.h", "../browser/chromeos/file_manager/file_manager_jstest_base.h",
......
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