blob: 219a4958628dc36d6ecd5aad19a64dfd78f65cbc [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 "src/developer/feedback_agent/feedback_agent.h"
#include <fuchsia/feedback/cpp/fidl.h>
#include <fuchsia/images/cpp/fidl.h>
#include <fuchsia/logger/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <lib/fidl/cpp/binding_set.h>
#include <lib/fidl/cpp/interface_handle.h>
#include <lib/fidl/cpp/interface_request.h>
#include <lib/fostr/fidl/fuchsia/math/formatting.h>
#include <lib/fostr/fidl/fuchsia/mem/formatting.h>
#include <lib/fostr/indent.h>
#include <lib/fsl/vmo/file.h>
#include <lib/fsl/vmo/sized_vmo.h>
#include <lib/fsl/vmo/strings.h>
#include <lib/fsl/vmo/vector.h>
#include <lib/gtest/real_loop_fixture.h>
#include <lib/sys/cpp/testing/service_directory_provider.h>
#include <lib/syslog/cpp/logger.h>
#include <lib/zx/time.h>
#include <lib/zx/vmo.h>
#include <stdint.h>
#include <zircon/errors.h>
#include <memory>
#include <ostream>
#include <vector>
#include "src/lib/files/file.h"
#include "src/lib/fxl/logging.h"
#include "third_party/googletest/googlemock/include/gmock/gmock.h"
#include "third_party/googletest/googletest/include/gtest/gtest.h"
namespace fuchsia {
namespace feedback {
namespace {
using fuchsia::ui::scenic::ScreenshotData;
constexpr bool kSuccess = true;
constexpr bool kFailure = false;
constexpr zx_time_t kSyslogBaseTimestamp = ZX_SEC(15604);
constexpr uint64_t kSyslogProcessId = 7559;
constexpr uint64_t kSyslogThreadId = 7687;
// Returns an empty screenshot, still needed when Scenic::TakeScreenshot()
// returns false as the FIDL ScreenshotData field is not marked optional in
// fuchsia.ui.scenic.Scenic.TakeScreenshot.
ScreenshotData CreateEmptyScreenshot() {
ScreenshotData screenshot;
FXL_CHECK(zx::vmo::create(0, 0u, &screenshot.data.vmo) == ZX_OK);
return screenshot;
}
struct RGBA {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
};
// Returns an 8-bit BGRA image of a |image_dim_in_px| x |image_dim_in_px|
// checkerboard, where each white/black region is a 10x10 pixel square.
ScreenshotData CreateCheckerboardScreenshot(const size_t image_dim_in_px) {
const size_t height = image_dim_in_px;
const size_t width = image_dim_in_px;
const size_t block_size = 10;
const uint8_t black = 0;
const uint8_t white = 0xff;
const size_t size_in_bytes = image_dim_in_px * image_dim_in_px * sizeof(RGBA);
auto ptr = std::make_unique<uint8_t[]>(size_in_bytes);
RGBA* pixels = reinterpret_cast<RGBA*>(ptr.get());
// We go pixel by pixel, row by row. |y| tracks the row and |x| the column.
//
// We compute in which |block_size| x |block_size| block the pixel is to
// determine the color (black or white). |block_y| tracks the "block" row and
// |block_x| the "block" column.
for (size_t y = 0; y < height; ++y) {
size_t block_y = y / block_size;
for (size_t x = 0; x < width; ++x) {
size_t block_x = x / block_size;
uint8_t block_color = (block_x + block_y) % 2 ? black : white;
size_t index = y * width + x;
auto& p = pixels[index];
p.r = p.g = p.b = block_color;
p.a = 255;
}
}
ScreenshotData screenshot;
FXL_CHECK(zx::vmo::create(size_in_bytes, 0u, &screenshot.data.vmo) == ZX_OK);
FXL_CHECK(screenshot.data.vmo.write(ptr.get(), 0u, size_in_bytes) == ZX_OK);
screenshot.data.size = size_in_bytes;
screenshot.info.height = image_dim_in_px;
screenshot.info.width = image_dim_in_px;
screenshot.info.stride = image_dim_in_px * 4u /*4 bytes per pixel*/;
screenshot.info.pixel_format = fuchsia::images::PixelFormat::BGRA_8;
return screenshot;
}
// Returns an empty screenshot with a pixel format different from BGRA-8.
ScreenshotData CreateNonBGRA8Screenshot() {
ScreenshotData screenshot = CreateEmptyScreenshot();
screenshot.info.pixel_format = fuchsia::images::PixelFormat::YUY2;
return screenshot;
}
// Returns a Screenshot with the right dimensions, no image.
std::unique_ptr<Screenshot> MakeUniqueScreenshot(const size_t image_dim_in_px) {
std::unique_ptr<Screenshot> screenshot = std::make_unique<Screenshot>();
screenshot->dimensions_in_px.height = image_dim_in_px;
screenshot->dimensions_in_px.width = image_dim_in_px;
return screenshot;
}
// Represents arguments for Scenic::TakeScreenshot().
struct TakeScreenshotResponse {
ScreenshotData screenshot;
bool success;
TakeScreenshotResponse(ScreenshotData data, bool success)
: screenshot(std::move(data)), success(success){};
};
// Represents arguments for DataProvider::GetScreenshotCallback.
struct GetScreenshotResponse {
std::unique_ptr<Screenshot> screenshot;
};
// Compares two GetScreenshotResponse.
template <typename ResultListenerT>
bool DoGetScreenshotResponseMatch(const GetScreenshotResponse& actual,
const GetScreenshotResponse& expected,
ResultListenerT* result_listener) {
if (actual.screenshot == nullptr && expected.screenshot == nullptr) {
return true;
}
if (actual.screenshot == nullptr && expected.screenshot != nullptr) {
*result_listener << "Got no screenshot, expected one";
return false;
}
if (expected.screenshot == nullptr && actual.screenshot != nullptr) {
*result_listener << "Expected no screenshot, got one";
return false;
}
// actual.screenshot and expected.screenshot are now valid.
if (!fidl::Equals(actual.screenshot->dimensions_in_px,
expected.screenshot->dimensions_in_px)) {
*result_listener << "Expected screenshot dimensions "
<< expected.screenshot->dimensions_in_px << ", got "
<< actual.screenshot->dimensions_in_px;
return false;
}
// We do not compare the VMOs.
return true;
}
// Returns true if gMock |arg| matches |expected|, assuming two
// GetScreenshotResponse.
MATCHER_P(MatchesGetScreenshotResponse, expected, "") {
return DoGetScreenshotResponseMatch(arg, expected, result_listener);
}
// Compares two Attachment.
template <typename ResultListenerT>
bool DoAttachmentMatch(const Attachment& actual,
const std::string& expected_key,
const std::string& expected_value,
ResultListenerT* result_listener) {
if (actual.key != expected_key) {
*result_listener << "Expected key " << expected_key << ", got "
<< actual.key;
return false;
}
std::string actual_value;
if (!fsl::StringFromVmo(actual.value, &actual_value)) {
*result_listener << "Cannot parse actual VMO for key " << actual.key
<< " to string";
return false;
}
if (actual_value.compare(expected_value) != 0) {
*result_listener << "Expected value " << expected_value << ", got "
<< actual_value;
return false;
}
return true;
}
// Returns true if gMock |arg|.key matches |expected_key| and str(|arg|.value)
// matches |expected_value|, assuming two Attachment.
MATCHER_P2(MatchesAttachment, expected_key, expected_value,
"matches an attachment with key '" + std::string(expected_key) +
"' and value '" + std::string(expected_value) + "'") {
return DoAttachmentMatch(arg, expected_key, expected_value, result_listener);
}
// Stub Scenic service to return canned responses to Scenic::TakeScreenshot().
class StubScenic : public fuchsia::ui::scenic::Scenic {
public:
// Returns a request handler for binding to this stub service.
fidl::InterfaceRequestHandler<fuchsia::ui::scenic::Scenic> GetHandler() {
return bindings_.GetHandler(this);
}
// Scenic methods.
void CreateSession(
fidl::InterfaceRequest<fuchsia::ui::scenic::Session> session,
fidl::InterfaceHandle<fuchsia::ui::scenic::SessionListener> listener)
override {
FXL_NOTIMPLEMENTED();
}
void GetDisplayInfo(GetDisplayInfoCallback callback) override {
FXL_NOTIMPLEMENTED();
}
void GetDisplayOwnershipEvent(
GetDisplayOwnershipEventCallback callback) override {
FXL_NOTIMPLEMENTED();
}
void TakeScreenshot(TakeScreenshotCallback callback) override {
FXL_CHECK(!take_screenshot_responses_.empty())
<< "You need to set up Scenic::TakeScreenshot() responses before "
"testing GetScreenshot() using set_scenic_responses()";
TakeScreenshotResponse response = std::move(take_screenshot_responses_[0]);
take_screenshot_responses_.erase(take_screenshot_responses_.begin());
callback(std::move(response.screenshot), response.success);
}
// Stub injection and verification methods.
void set_take_screenshot_responses(
std::vector<TakeScreenshotResponse> responses) {
take_screenshot_responses_ = std::move(responses);
}
const std::vector<TakeScreenshotResponse>& take_screenshot_responses() const {
return take_screenshot_responses_;
}
private:
fidl::BindingSet<fuchsia::ui::scenic::Scenic> bindings_;
std::vector<TakeScreenshotResponse> take_screenshot_responses_;
};
// Stub Log service to return canned responses to Log::DumpLogs().
class StubLogger : public fuchsia::logger::Log {
public:
// Returns a request handler for binding to this stub service.
fidl::InterfaceRequestHandler<fuchsia::logger::Log> GetHandler() {
return bindings_.GetHandler(this);
}
// fuchsia::logger::Log methods.
void Listen(
fidl::InterfaceHandle<fuchsia::logger::LogListener> log_listener,
std::unique_ptr<fuchsia::logger::LogFilterOptions> options) override {
FXL_NOTIMPLEMENTED();
}
void DumpLogs(
fidl::InterfaceHandle<fuchsia::logger::LogListener> log_listener,
std::unique_ptr<fuchsia::logger::LogFilterOptions> options) override {
fuchsia::logger::LogListenerPtr log_listener_ptr = log_listener.Bind();
FXL_CHECK(log_listener_ptr.is_bound());
log_listener_ptr->LogMany(messages_);
log_listener_ptr->Done();
}
// Stub injection methods.
void set_messages(const std::vector<fuchsia::logger::LogMessage>& messages) {
messages_ = messages;
}
private:
fidl::BindingSet<fuchsia::logger::Log> bindings_;
std::vector<fuchsia::logger::LogMessage> messages_;
};
// Unit-tests the implementation of the fuchsia.feedback.DataProvider FIDL
// interface.
//
// This does not test the environment service. It directly instantiates the
// class, without connecting through FIDL.
class FeedbackAgentTest : public gtest::RealLoopFixture {
public:
void SetUp() override {
stub_scenic_.reset(new StubScenic());
FXL_CHECK(service_directory_provider_.AddService(
stub_scenic_->GetHandler()) == ZX_OK);
stub_logger_.reset(new StubLogger());
FXL_CHECK(service_directory_provider_.AddService(
stub_logger_->GetHandler()) == ZX_OK);
agent_.reset(new FeedbackAgent(
dispatcher(), service_directory_provider_.service_directory()));
}
protected:
void set_scenic_responses(std::vector<TakeScreenshotResponse> responses) {
stub_scenic_->set_take_screenshot_responses(std::move(responses));
}
const std::vector<TakeScreenshotResponse>& get_scenic_responses() const {
return stub_scenic_->take_screenshot_responses();
}
void set_logger_messages(
const std::vector<fuchsia::logger::LogMessage>& messages) {
stub_logger_->set_messages(messages);
}
std::unique_ptr<FeedbackAgent> agent_;
private:
::sys::testing::ServiceDirectoryProvider service_directory_provider_;
std::unique_ptr<StubScenic> stub_scenic_;
std::unique_ptr<StubLogger> stub_logger_;
};
TEST_F(FeedbackAgentTest, GetScreenshot_SucceedOnScenicReturningSuccess) {
const size_t image_dim_in_px = 100;
std::vector<TakeScreenshotResponse> scenic_responses;
scenic_responses.emplace_back(CreateCheckerboardScreenshot(image_dim_in_px),
kSuccess);
set_scenic_responses(std::move(scenic_responses));
GetScreenshotResponse feedback_response;
agent_->GetScreenshot(
ImageEncoding::PNG,
[&feedback_response](std::unique_ptr<Screenshot> screenshot) {
feedback_response.screenshot = std::move(screenshot);
});
RunLoopUntilIdle();
EXPECT_TRUE(get_scenic_responses().empty());
ASSERT_NE(feedback_response.screenshot, nullptr);
EXPECT_EQ((size_t)feedback_response.screenshot->dimensions_in_px.height,
image_dim_in_px);
EXPECT_EQ((size_t)feedback_response.screenshot->dimensions_in_px.width,
image_dim_in_px);
EXPECT_TRUE(feedback_response.screenshot->image.vmo.is_valid());
fsl::SizedVmo expected_sized_vmo;
ASSERT_TRUE(fsl::VmoFromFilename("/pkg/data/checkerboard_100.png",
&expected_sized_vmo));
std::vector<uint8_t> expected_pixels;
ASSERT_TRUE(fsl::VectorFromVmo(expected_sized_vmo, &expected_pixels));
std::vector<uint8_t> actual_pixels;
ASSERT_TRUE(
fsl::VectorFromVmo(feedback_response.screenshot->image, &actual_pixels));
EXPECT_EQ(actual_pixels, expected_pixels);
}
TEST_F(FeedbackAgentTest, GetScreenshot_FailOnScenicReturningFailure) {
std::vector<TakeScreenshotResponse> scenic_responses;
scenic_responses.emplace_back(CreateEmptyScreenshot(), kFailure);
set_scenic_responses(std::move(scenic_responses));
GetScreenshotResponse feedback_response;
agent_->GetScreenshot(
ImageEncoding::PNG,
[&feedback_response](std::unique_ptr<Screenshot> screenshot) {
feedback_response.screenshot = std::move(screenshot);
});
RunLoopUntilIdle();
EXPECT_TRUE(get_scenic_responses().empty());
EXPECT_EQ(feedback_response.screenshot, nullptr);
}
TEST_F(FeedbackAgentTest,
GetScreenshot_FailOnScenicReturningNonBGRA8Screenshot) {
std::vector<TakeScreenshotResponse> scenic_responses;
scenic_responses.emplace_back(CreateNonBGRA8Screenshot(), kSuccess);
set_scenic_responses(std::move(scenic_responses));
GetScreenshotResponse feedback_response;
agent_->GetScreenshot(
ImageEncoding::PNG,
[&feedback_response](std::unique_ptr<Screenshot> screenshot) {
feedback_response.screenshot = std::move(screenshot);
});
RunLoopUntilIdle();
EXPECT_TRUE(get_scenic_responses().empty());
EXPECT_EQ(feedback_response.screenshot, nullptr);
}
TEST_F(FeedbackAgentTest, GetScreenshot_ParallelRequests) {
// We simulate three calls to FeedbackAgent::GetScreenshot(): one for which
// the stub Scenic will return a checkerboard 10x10, one for a 20x20 and one
// failure.
const size_t num_calls = 3u;
const size_t image_dim_in_px_0 = 10u;
const size_t image_dim_in_px_1 = 20u;
std::vector<TakeScreenshotResponse> scenic_responses;
scenic_responses.emplace_back(CreateCheckerboardScreenshot(image_dim_in_px_0),
kSuccess);
scenic_responses.emplace_back(CreateCheckerboardScreenshot(image_dim_in_px_1),
kSuccess);
scenic_responses.emplace_back(CreateEmptyScreenshot(), kFailure);
ASSERT_EQ(scenic_responses.size(), num_calls);
set_scenic_responses(std::move(scenic_responses));
std::vector<GetScreenshotResponse> feedback_responses;
for (size_t i = 0; i < num_calls; i++) {
agent_->GetScreenshot(
ImageEncoding::PNG,
[&feedback_responses](std::unique_ptr<Screenshot> screenshot) {
feedback_responses.push_back({std::move(screenshot)});
});
}
RunLoopUntilIdle();
EXPECT_TRUE(get_scenic_responses().empty());
// We cannot assume that the order of the FeedbackAgent::GetScreenshot()
// calls match the order of the Scenic::TakeScreenshot() callbacks because of
// the async message loop. Thus we need to match them as sets.
//
// We set the expectations in advance and then pass a reference to the gMock
// matcher using testing::ByRef() because the underlying VMO is not copyable.
const GetScreenshotResponse expected_0 = {
MakeUniqueScreenshot(image_dim_in_px_0)};
const GetScreenshotResponse expected_1 = {
MakeUniqueScreenshot(image_dim_in_px_1)};
const GetScreenshotResponse expected_2 = {nullptr};
EXPECT_THAT(feedback_responses,
testing::UnorderedElementsAreArray({
MatchesGetScreenshotResponse(testing::ByRef(expected_0)),
MatchesGetScreenshotResponse(testing::ByRef(expected_1)),
MatchesGetScreenshotResponse(testing::ByRef(expected_2)),
}));
// Additionally, we check that in the non-empty responses, the VMO is valid.
for (const auto& response : feedback_responses) {
if (response.screenshot == nullptr) {
continue;
}
EXPECT_TRUE(response.screenshot->image.vmo.is_valid());
EXPECT_GE(response.screenshot->image.size, 0u);
}
}
fuchsia::logger::LogMessage BuildLogMessage(
const int32_t severity, const std::string& text,
const zx_time_t timestamp_offset,
const std::vector<std::string>& tags = {}) {
fuchsia::logger::LogMessage msg{};
msg.time = kSyslogBaseTimestamp + timestamp_offset;
msg.pid = kSyslogProcessId;
msg.tid = kSyslogThreadId;
msg.tags = tags;
msg.severity = severity;
msg.msg = text;
return msg;
}
TEST_F(FeedbackAgentTest, GetData_SmokeTest) {
set_logger_messages({
BuildLogMessage(0 /*INFO*/, "line 1", 0),
BuildLogMessage(1 /*WARN*/, "line 2", ZX_MSEC(1)),
BuildLogMessage(2 /*ERROR*/, "line 3", ZX_MSEC(2)),
BuildLogMessage(3 /*FATAL*/, "line 4", ZX_MSEC(3)),
BuildLogMessage(-1 /*VLOG(1)*/, "line 5", ZX_MSEC(4)),
BuildLogMessage(-2 /*VLOG(2)*/, "line 6", ZX_MSEC(5)),
BuildLogMessage(0 /*INFO*/, "line 7", ZX_MSEC(6), /*tags=*/{"foo"}),
BuildLogMessage(0 /*INFO*/, "line 8", ZX_MSEC(7), /*tags=*/{"bar"}),
BuildLogMessage(0 /*INFO*/, "line 9", ZX_MSEC(8),
/*tags=*/{"foo", "bar"}),
});
DataProvider_GetData_Result feedback_result;
agent_->GetData([&feedback_result](DataProvider_GetData_Result result) {
feedback_result = std::move(result);
});
RunLoopUntilIdle();
ASSERT_TRUE(feedback_result.is_response());
// As we control the system log attachment, we can expect it to be present and
// with a particular value.
ASSERT_TRUE(feedback_result.response().data.has_attachments());
EXPECT_THAT(feedback_result.response().data.attachments(),
testing::Contains(
MatchesAttachment("log.system",
R"([15604.000][07559][07687][] INFO: line 1
[15604.001][07559][07687][] WARN: line 2
[15604.002][07559][07687][] ERROR: line 3
[15604.003][07559][07687][] FATAL: line 4
[15604.004][07559][07687][] VLOG(1): line 5
[15604.005][07559][07687][] VLOG(2): line 6
[15604.006][07559][07687][foo] INFO: line 7
[15604.007][07559][07687][bar] INFO: line 8
[15604.008][07559][07687][foo, bar] INFO: line 9
)")));
// There is nothing else we can assert here as no missing annotation nor
// attachment is fatal.
}
} // namespace
// Pretty-prints Attachment in gTest matchers instead of the default byte string
// in case of failed expectations.
void PrintTo(const Attachment& attachment, std::ostream* os) {
*os << fostr::Indent;
*os << fostr::NewLine << "key: " << attachment.key;
*os << fostr::NewLine << "value: ";
std::string value;
if (fsl::StringFromVmo(attachment.value, &value)) {
if (value.size() < 1024) {
*os << "'" << value << "'";
} else {
*os << "(string too long)" << attachment.value;
}
} else {
*os << attachment.value;
}
*os << fostr::Outdent;
}
} // namespace feedback
} // namespace fuchsia
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
syslog::InitLogger({"feedback_agent", "test"});
return RUN_ALL_TESTS();
}