blob: 143676232563560ff1d07f6d2dcdf0de9f83efef [file] [log] [blame]
// Copyright 2018 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 <fstream>
#include <map>
#include <ostream>
#include <string>
#include <vector>
#include <fuchsia/ui/policy/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <gtest/gtest.h>
#include <lib/async/cpp/task.h>
#include <lib/component/cpp/testing/test_with_environment.h>
#include <lib/fsl/vmo/vector.h>
#include <lib/fxl/logging.h>
#include <lib/fxl/strings/string_printf.h>
#include <lib/ui/gfx/cpp/math.h>
#include <lib/ui/scenic/cpp/resources.h>
#include <lib/ui/scenic/cpp/session.h>
namespace {
constexpr char kEnvironment[] = "ScenicPixelTest";
// These tests need Scenic and RootPresenter at minimum, which expand to the
// dependencies below. Using |TestWithEnvironment|, we use
// |fuchsia.sys.Environment| and |fuchsia.sys.Loader| from the system (declared
// in our *.cmx sandbox) and launch these other services in the environment we
// create in our test fixture.
//
// Another way to do this would be to whitelist these services in our sandbox
// and inject/start them via the |fuchsia.test| facet. However that has the
// disadvantage that it uses one instance of those services across all tests in
// the binary, making each test not hermetic wrt. the others. A trade-off is
// that the |TestWithEnvironment| method is more verbose.
const std::map<std::string, std::string> kServices = {
{"fuchsia.tracelink.Registry",
"fuchsia-pkg://fuchsia.com/trace_manager#meta/trace_manager.cmx"},
{"fuchsia.ui.policy.Presenter2",
"fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx"},
{"fuchsia.ui.scenic.Scenic",
"fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cmx"},
{"fuchsia.vulkan.loader.Loader",
"fuchsia-pkg://fuchsia.com/vulkan_loader#meta/vulkan_loader.cmx"}};
struct ViewContext {
scenic::SessionPtrAndListenerRequest session_and_listener_request;
zx::eventpair view_token;
};
// Represents a view that allows a callback to be set for its |Present|.
class TestView {
public:
virtual ~TestView() = default;
virtual void set_present_callback(
scenic::Session::PresentCallback present_callback) = 0;
};
// Test fixture that sets up an environment suitable for Scenic pixel tests
// and provides related utilities. The environment includes Scenic and
// RootPresenter, and their dependencies.
class ScenicPixelTest : public component::testing::TestWithEnvironment {
protected:
ScenicPixelTest() {
std::unique_ptr<component::testing::EnvironmentServices> services =
CreateServices();
for (const auto& entry : kServices) {
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = entry.second;
services->AddServiceWithLaunchInfo(std::move(launch_info), entry.first);
}
environment_ =
CreateNewEnclosingEnvironment(kEnvironment, std::move(services));
environment_->ConnectToService(scenic_.NewRequest());
scenic_.set_error_handler([this](zx_status_t status) {
FAIL() << "Lost connection to Scenic: " << status;
});
}
// Blocking wrapper around |Scenic::TakeScreenshot|. This should not be called
// from within a loop |Run|, as it spins up its own to block and nested loops
// are undefined behavior.
fuchsia::ui::scenic::ScreenshotData TakeScreenshot() {
fuchsia::ui::scenic::ScreenshotData screenshot_out;
scenic_->TakeScreenshot(
[this, &screenshot_out](fuchsia::ui::scenic::ScreenshotData screenshot,
bool status) {
EXPECT_TRUE(status) << "Failed to take screenshot";
screenshot_out = std::move(screenshot);
QuitLoop();
});
RunLoop();
return screenshot_out;
}
// Dumps the screenshot to a shared temporary file and returns the filename.
std::string DumpScreenshot(fuchsia::ui::scenic::ScreenshotData screenshot) {
std::vector<uint8_t> data;
EXPECT_TRUE(fsl::VectorFromVmo(screenshot.data, &data))
<< "Failed to read screenshot";
const auto* test_info =
testing::UnitTest::GetInstance()->current_test_info();
std::ostringstream filename;
filename << "/tmp/screenshot-" << test_info->test_case_name()
<< "::" << test_info->name() << "-" << screenshot_index_;
std::ofstream fout(filename.str());
EXPECT_TRUE(fout.good())
<< "Failed to open " << filename.str() << " for writing";
// Convert to PPM. We'll probably need PNG instead for Skia Gold.
fout << "P6\n"
<< screenshot.info.width << "\n"
<< screenshot.info.height << "\n"
<< 255 << "\n";
const uint8_t* pchannel = data.data();
for (uint32_t pixel = 0;
pixel < screenshot.info.width * screenshot.info.height; pixel++) {
uint8_t rgb[] = {pchannel[2], pchannel[1], pchannel[0]};
fout.write(reinterpret_cast<const char*>(rgb), 3);
pchannel += 4;
}
fout.flush();
EXPECT_TRUE(fout.good())
<< "Failed to write screenshot to " << filename.str();
++screenshot_index_;
return filename.str();
}
// Create a |ViewContext| that allows us to present a view via
// |RootPresenter|. See also examples/ui/hello_base_view
ViewContext CreatePresentationContext() {
zx::eventpair view_holder_token, view_token;
FXL_CHECK(zx::eventpair::create(0u, &view_holder_token, &view_token) ==
ZX_OK);
ViewContext view_context = {
.session_and_listener_request =
scenic::CreateScenicSessionPtrAndListenerRequest(scenic_.get()),
.view_token = std::move(view_token),
};
fuchsia::ui::policy::Presenter2Ptr presenter;
environment_->ConnectToService(presenter.NewRequest());
presenter->PresentView(std::move(view_holder_token), nullptr);
return view_context;
}
// Runs until the view renders its next frame. Technically, waits until the
// |Present| callback is invoked with an expected presentation timestamp, and
// then waits until that time.
void RunUntilPresent(TestView* view) {
view->set_present_callback([this](fuchsia::images::PresentationInfo info) {
async::PostTaskForTime(dispatcher(), QuitLoopClosure(),
static_cast<zx::time>(info.presentation_time));
});
RunLoop();
}
fuchsia::ui::scenic::ScenicPtr scenic_;
private:
std::unique_ptr<component::testing::EnclosingEnvironment> environment_;
uint screenshot_index_ = 0;
};
struct Color {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
};
inline bool operator==(const Color& a, const Color& b) {
return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a;
}
inline bool operator<(const Color& a, const Color& b) {
return std::tie(a.r, a.g, a.b, a.a) < std::tie(b.r, b.g, b.b, b.a);
}
// RGBA hex dump
std::ostream& operator<<(std::ostream& os, const Color& c) {
return os << fxl::StringPrintf("%02X%02X%02X%02X", c.r, c.g, c.b, c.a);
}
// Base view with a solid background.
// See also lib/ui/base_view
class BackgroundView : public TestView,
private fuchsia::ui::scenic::SessionListener {
public:
static constexpr float kBackgroundElevation = 0.f;
static constexpr Color kBackgroundColor = {0x67, 0x3a, 0xb7,
0xff}; // Deep Purple 500
BackgroundView(ViewContext context,
const std::string& debug_name = "BackgroundView")
: binding_(this, std::move(context.session_and_listener_request.second)),
session_(std::move(context.session_and_listener_request.first)),
view_(&session_, std::move(context.view_token), debug_name),
background_node_(&session_) {
binding_.set_error_handler([](zx_status_t status) { FAIL() << status; });
session_.Present(0, [](auto) {});
scenic::Material background_material(&session_);
background_material.SetColor(kBackgroundColor.r, kBackgroundColor.g,
kBackgroundColor.b, kBackgroundColor.a);
background_node_.SetMaterial(background_material);
view_.AddChild(background_node_);
}
void set_present_callback(
scenic::Session::PresentCallback present_callback) override {
present_callback_ = std::move(present_callback);
}
protected:
scenic::Session* session() { return &session_; }
scenic::View* view() { return &view_; }
virtual void Draw(float cx, float cy, float sx, float sy) {
scenic::Rectangle background_shape(&session_, sx, sy);
background_node_.SetShape(background_shape);
background_node_.SetTranslation((float[]){cx, cy, kBackgroundElevation});
}
private:
void OnScenicEvent(std::vector<fuchsia::ui::scenic::Event> events) override {
for (const auto& event : events) {
if (event.Which() == fuchsia::ui::scenic::Event::Tag::kGfx &&
event.gfx().Which() ==
fuchsia::ui::gfx::Event::Tag::kViewPropertiesChanged) {
const auto& evt = event.gfx().view_properties_changed();
fuchsia::ui::gfx::BoundingBox layout_box =
scenic::ViewPropertiesLayoutBox(evt.properties);
const auto sz = scenic::Max(layout_box.max - layout_box.min, 0.f);
OnViewPropertiesChanged(sz);
}
}
}
void OnScenicError(std::string error) override { FAIL() << error; }
void OnViewPropertiesChanged(const fuchsia::ui::gfx::vec3& sz) {
if (!sz.x || !sz.y || !sz.z)
return;
Draw(sz.x * .5f, sz.y * .5f, sz.x, sz.y);
session_.Present(0, std::move(present_callback_));
}
fidl::Binding<fuchsia::ui::scenic::SessionListener> binding_;
scenic::Session session_;
scenic::View view_;
scenic::ShapeNode background_node_;
scenic::Session::PresentCallback present_callback_;
};
// Displays a static frame of the Spinning Square example.
// See also examples/ui/spinning_square
class RotatedSquareView : public BackgroundView {
public:
static constexpr float kSquareElevation = 8.f;
static constexpr float kAngle = M_PI / 4;
RotatedSquareView(ViewContext context,
const std::string& debug_name = "RotatedSquareView")
: BackgroundView(std::move(context), debug_name),
square_node_(session()) {
scenic::Material square_material(session());
square_material.SetColor(0xf5, 0x00, 0x57, 0xff); // Pink A400
square_node_.SetMaterial(square_material);
view()->AddChild(square_node_);
}
private:
void Draw(float cx, float cy, float sx, float sy) override {
BackgroundView::Draw(cx, cy, sx, sy);
const float square_size = std::min(sx, sy) * .6f;
scenic::Rectangle square_shape(session(), square_size, square_size);
square_node_.SetShape(square_shape);
square_node_.SetTranslation((float[]){cx, cy, kSquareElevation});
square_node_.SetRotation(
(float[]){0.f, 0.f, sinf(kAngle * .5f), cosf(kAngle * .5f)});
}
scenic::ShapeNode square_node_;
};
TEST_F(ScenicPixelTest, SolidColor) {
BackgroundView view(CreatePresentationContext());
RunUntilPresent(&view);
fuchsia::ui::scenic::ScreenshotData screenshot = TakeScreenshot();
std::vector<uint8_t> data;
EXPECT_TRUE(fsl::VectorFromVmo(screenshot.data, &data))
<< "Failed to read screenshot";
EXPECT_GT(screenshot.info.width, 0u);
EXPECT_GT(screenshot.info.height, 0u);
// We could assert on each pixel individually, but a histogram might give us a
// more meaningful failure.
std::map<Color, size_t> histogram;
// https://en.wikipedia.org/wiki/Sword_Art_Online_Alternative_Gun_Gale_Online#Characters
const uint8_t* pchan = data.data();
const size_t llenn = screenshot.info.width * screenshot.info.height;
for (uint32_t pixel = 0; pixel < llenn; pixel++) {
Color color = {pchan[2], pchan[1], pchan[0], pchan[3]};
++histogram[color];
pchan += 4;
}
EXPECT_GT(histogram[BackgroundView::kBackgroundColor], 0u);
histogram.erase(BackgroundView::kBackgroundColor);
EXPECT_EQ((std::map<Color, size_t>){}, histogram) << "Unexpected colors";
}
// Renders a rotated square to the screen and saves a screenshot for later
// screendiff validation.
TEST_F(ScenicPixelTest, RotatedSquare) {
RotatedSquareView view(CreatePresentationContext());
RunUntilPresent(&view);
const std::string filename = DumpScreenshot(TakeScreenshot());
FXL_LOG(INFO) << "Wrote screenshot to " << filename;
}
} // namespace