| // 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 <fuchsia/ui/composition/cpp/fidl.h> |
| #include <fuchsia/ui/test/input/cpp/fidl.h> |
| |
| #include <fstream> |
| #include <iostream> |
| #include <string> |
| #include <unordered_set> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| #include <rapidjson/document.h> |
| |
| #include "lib/zx/clock.h" |
| #include "lib/zx/time.h" |
| #include "src/lib/fxl/strings/string_printf.h" |
| #include "src/virtualization/tests/lib/enclosed_guest.h" |
| #include "src/virtualization/tests/lib/guest_test.h" |
| #include "src/virtualization/tests/lib/periodic_logger.h" |
| |
| namespace { |
| |
| using testing::AllOf; |
| using testing::Ge; |
| using testing::Le; |
| |
| // Set to "true" to save screenshots to disk. |
| constexpr bool kSaveScreenshot = false; |
| constexpr char kScreenshotSaveLocation[] = "/tmp/screenshot-%s.raw"; |
| |
| constexpr char kVirtioGpuTestUtil[] = "virtio_gpu_test_util"; |
| |
| // How long to run tests before giving up and failing. |
| constexpr zx::duration kGpuTestTimeout = zx::sec(15); |
| |
| struct Screenshot { |
| // Height and width of the image, in pixels. |
| int height; |
| int width; |
| |
| // Raw pixel data, 4 bytes per pixel, stored in RGBO format, one row at |
| // a time. |
| std::vector<std::byte> data; |
| }; |
| |
| template <typename T> |
| class VirtioGpuTest : public GuestTest<T> { |
| protected: |
| void SetUp() override { |
| GuestTest<T>::SetUp(); |
| screenshot_ = |
| this->GetEnclosedGuest().template ConnectToService<fuchsia::ui::composition::Screenshot>(); |
| display_info_ = this->GetEnclosedGuest().WaitForDisplay(); |
| } |
| |
| zx_status_t CaptureScreenshot(Screenshot* screenshot) { |
| fuchsia::ui::composition::ScreenshotTakeRequest request; |
| request.set_format(fuchsia::ui::composition::ScreenshotFormat::BGRA_RAW); |
| std::optional<zx_status_t> screenshot_result; |
| screenshot_->Take(std::move(request), [screenshot, &screenshot_result](auto response) { |
| screenshot->width = response.size().width; |
| screenshot->height = response.size().height; |
| |
| size_t size = screenshot->width * screenshot->height * 4; |
| screenshot->data.reserve(size); |
| zx_status_t status = response.vmo().read(screenshot->data.data(), 0, size); |
| if (status != ZX_OK) { |
| screenshot_result = status; |
| } |
| screenshot_result = ZX_OK; |
| }); |
| |
| if (!this->RunLoopUntil([&] { return screenshot_result.has_value(); }, |
| zx::deadline_after(kGpuTestTimeout))) { |
| return ZX_ERR_TIMED_OUT; |
| } |
| return *screenshot_result; |
| } |
| |
| EnclosedGuest::DisplayInfo display_info_; |
| fuchsia::ui::composition::ScreenshotPtr screenshot_; |
| }; |
| |
| using GuestTypes = ::testing::Types<DebianGpuEnclosedGuest, ZirconGpuEnclosedGuest>; |
| TYPED_TEST_SUITE(VirtioGpuTest, GuestTypes, GuestTestNameGenerator); |
| |
| // Poll |condition| using exponential backoff until it returns true, or |
| // a timeout has passed. |
| // |
| // Returns true iff |condition| returned true before the deadline. |
| template <typename C> |
| bool PollCondition(C condition, zx::duration timeout, PeriodicLogger logger) { |
| const zx::time deadline = zx::deadline_after(timeout); |
| zx::duration wait_time = zx::usec(1); |
| |
| do { |
| // Have we succeeded? |
| if (condition()) { |
| return true; |
| } |
| |
| // Sleep, with exponential backoff. |
| zx::nanosleep(zx::deadline_after(wait_time)); |
| wait_time = std::max(wait_time * 2, zx::sec(1)); |
| } while (zx::clock::get_monotonic() < deadline); |
| |
| // Perform one final check. |
| return condition(); |
| } |
| |
| // compiled in. |
| void SaveScreenshot(const std::string& prefix, const Screenshot& screenshot) { |
| if (kSaveScreenshot) { |
| std::string filename = fxl::StringPrintf(kScreenshotSaveLocation, prefix.c_str()); |
| FX_LOGS(INFO) << fxl::StringPrintf( |
| "Saving screenshot to '%s'. Copy from the device using:\n" |
| "# fx scp \"[$(fx get-device-addr)]\":%s data.raw\n" |
| "Display it using ImageMagick using one of the following commands.\n" |
| "Linux guests:\n" |
| "# display -size %dx%d -depth 8 RGBO:data.raw\n" |
| "Fuchsia guests:\n" |
| "# display -size %dx%d -depth 8 RGBA:data.raw\n", |
| filename.c_str(), filename.c_str(), screenshot.width, screenshot.height, screenshot.width, |
| screenshot.height); |
| std::ofstream output; |
| output.open(filename); |
| output.write(reinterpret_cast<const char*>(screenshot.data.data()), screenshot.data.size()); |
| output.close(); |
| } |
| } |
| |
| // Determine if the given screenshot has at least 1 non-black pixel. |
| // |
| // We assume the data format is RGBA or RGBO, where each pixel is four bytes: |
| // [red] [green] [blue] [alpha/opacity] |
| bool HasNonBlackPixel(const Screenshot& screenshot) { |
| FX_CHECK(screenshot.data.size() % 4 == 0); |
| for (size_t i = 0; i < screenshot.data.size(); i += 4) { |
| std::byte r = screenshot.data[i + 0]; |
| std::byte g = screenshot.data[i + 1]; |
| std::byte b = screenshot.data[i + 2]; |
| if (r != std::byte(0) || g != std::byte(0) || b != std::byte(0)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Count number of unique colours in the screenshot. |
| // |
| // For this test, we treat data as having different alpha values as different |
| // colours. |
| int NumberOfUniqueColors(const Screenshot& screenshot) { |
| FX_CHECK(screenshot.data.size() % 4 == 0); |
| std::unordered_set<uint32_t> seen_colors; |
| int unique_colors = 0; |
| for (size_t i = 0; i < screenshot.data.size(); i += 4) { |
| uint32_t color = *reinterpret_cast<const uint32_t*>(&screenshot.data[i]); |
| auto [_, new_color] = seen_colors.insert(color); |
| if (new_color) { |
| unique_colors++; |
| } |
| } |
| return unique_colors; |
| } |
| |
| bool ScreenshotsSame(const Screenshot& a, const Screenshot& b) { |
| if (a.height != b.height || a.width != b.width) { |
| return false; |
| } |
| return a.data == b.data; |
| } |
| |
| TYPED_TEST(VirtioGpuTest, DetectDisplay) { |
| // TODO(https://fxbug.dev/42080718): Fix and re-enable this test on zircon. |
| if (this->GetGuestKernel() == GuestKernel::ZIRCON) { |
| GTEST_SKIP(); |
| } |
| |
| std::string result; |
| ASSERT_EQ(this->RunUtil(kVirtioGpuTestUtil, {"detect"}, &result), ZX_OK); |
| |
| // Expect that a single display was detected, and the geometry should match that of the created |
| // view. |
| // |
| // The output is expected to look like: |
| // { |
| // "displays": [ |
| // { |
| // "width": <width>, |
| // "height": <height> |
| // } |
| // ] |
| // } |
| // |
| // The width and height are expected to also match the size of the backing Fuchsia view. |
| rapidjson::Document document; |
| document.Parse(result); |
| EXPECT_TRUE(document.IsObject()); |
| EXPECT_TRUE(document.HasMember("displays")); |
| EXPECT_TRUE(document["displays"].IsArray()); |
| |
| const auto& displays = document["displays"].GetArray(); |
| EXPECT_EQ(1u, displays.Size()); |
| EXPECT_TRUE(displays[0].IsObject()); |
| |
| const auto& display = displays[0]; |
| EXPECT_TRUE(display.HasMember("width") && display["width"].IsUint()); |
| EXPECT_TRUE(display.HasMember("height") && display["height"].IsUint()); |
| EXPECT_EQ(this->display_info_.width, display["width"].GetUint()); |
| EXPECT_EQ(this->display_info_.height, display["height"].GetUint()); |
| } |
| |
| TYPED_TEST(VirtioGpuTest, ScreenNotBlack) { |
| // TODO(https://fxbug.dev/42053853): Revive the this test. |
| GTEST_SKIP(); |
| |
| // Take a screenshot. |
| Screenshot screenshot; |
| zx_status_t status = this->CaptureScreenshot(&screenshot); |
| ASSERT_EQ(status, ZX_OK) << "Error capturing screenshot."; |
| SaveScreenshot("screen-not-black", screenshot); |
| |
| // Ensure that at least 1 pixel is not black. |
| EXPECT_TRUE(HasNonBlackPixel(screenshot)) << "All pixels in the captured screenshot were black."; |
| } |
| |
| TYPED_TEST(VirtioGpuTest, ScreenDataLooksValid) { |
| // TODO(https://fxbug.dev/42053853): Revive the this test. |
| GTEST_SKIP(); |
| |
| // Take a screenshot. |
| Screenshot screenshot; |
| zx_status_t status = this->CaptureScreenshot(&screenshot); |
| ASSERT_EQ(status, ZX_OK) << "Error capturing screenshot."; |
| SaveScreenshot("unique-colors", screenshot); |
| |
| // Ensure that we have at least 2 distinct colours, but no more than 16. The |
| // idea here is that we want to ensure the screen is showing _something_ |
| // (white text on a black background), but not complete garbage |
| // (uninitialised memory, for example). |
| // |
| // Both Zircon and Linux guests have a simple console on bootup, so ensuring |
| // that we only have a few unique colours lets us approximate this. |
| // |
| // If you've just added a beautiful rainbow to Fuchsia's console and now |
| // this test is failing, I'm really, truly sorry. |
| EXPECT_THAT(NumberOfUniqueColors(screenshot), AllOf(Ge(2), Le(16))) |
| << "The screenshot had a suspicious number of colours, suggesting it " |
| "may not actually be real screen content."; |
| } |
| |
| TYPED_TEST(VirtioGpuTest, TextInputChangesConsole) { |
| // TODO(https://fxbug.dev/42053853): Revive the this test. |
| GTEST_SKIP(); |
| |
| // Take a screenshot. |
| Screenshot screenshot1; |
| zx_status_t status = this->CaptureScreenshot(&screenshot1); |
| ASSERT_EQ(status, ZX_OK) << "Error capturing screenshot."; |
| SaveScreenshot("input-state1", screenshot1); |
| |
| // Type a key, which should update the display. |
| auto input_registry = |
| this->GetEnclosedGuest().template ConnectToService<fuchsia::ui::test::input::Registry>(); |
| fuchsia::ui::test::input::KeyboardPtr keyboard; |
| bool keyboard_registered = false; |
| fuchsia::ui::test::input::RegistryRegisterKeyboardRequest request; |
| request.set_device(keyboard.NewRequest()); |
| input_registry->RegisterKeyboard(std::move(request), [&]() { keyboard_registered = true; }); |
| this->RunLoopUntil([&] { return keyboard_registered; }, zx::deadline_after(kGpuTestTimeout)); |
| bool input_injected = false; |
| fuchsia::ui::test::input::KeyboardSimulateUsAsciiTextEntryRequest text_request; |
| text_request.set_text("a"); |
| keyboard->SimulateUsAsciiTextEntry(std::move(text_request), |
| [&input_injected]() { input_injected = true; }); |
| this->RunLoopUntil([&] { return input_injected; }, zx::deadline_after(kGpuTestTimeout)); |
| |
| // Take another screenshot. |
| // |
| // We keep polling to handle any delay in propagating input to output. |
| bool success = PollCondition( |
| [&screenshot1]() -> bool { |
| // Wait for the screen to change. |
| Screenshot screenshot2; |
| SaveScreenshot("input-state2", screenshot2); |
| return !ScreenshotsSame(screenshot1, screenshot2); |
| }, |
| /*timeout=*/kGpuTestTimeout, PeriodicLogger("Waiting for change in console", zx::sec(1))); |
| |
| // Ensure something changed. |
| EXPECT_TRUE(success) |
| << "Expected keystroke events to change console output, but nothing changed."; |
| } |
| |
| } // namespace |