| // 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 <fstream> | |
| #include <iostream> | |
| #include <string> | |
| #include <unordered_set> | |
| #include <gmock/gmock.h> | |
| #include <gtest/gtest.h> | |
| #include "lib/zx/clock.h" | |
| #include "lib/zx/time.h" | |
| #include "src/lib/fxl/strings/string_printf.h" | |
| #include "src/virtualization/tests/enclosed_guest.h" | |
| #include "src/virtualization/tests/fake_scenic.h" | |
| #include "src/virtualization/tests/guest_test.h" | |
| #include "src/virtualization/tests/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"; | |
| // How long to run tests before giving up and failing. | |
| constexpr zx::duration kGpuTestTimeout = zx::sec(15); | |
| template <typename T> | |
| using VirtioGpuTest = GuestTest<T>; | |
| using GuestTypes = ::testing::Types<ZirconEnclosedGuest, DebianEnclosedGuest>; | |
| TYPED_TEST_SUITE(VirtioGpuTest, GuestTypes); | |
| // 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, std::optional<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; | |
| } | |
| logger->LogIfRequired(); | |
| // 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(); | |
| } | |
| // Save a screenshot to disk, if the constand "kSaveScreeshot" has been | |
| // 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, ScreenNotBlack) { | |
| // Take a screenshot. | |
| Screenshot screenshot; | |
| zx_status_t status = this->GetEnclosedGuest()->GetScenic()->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) { | |
| // Take a screenshot. | |
| Screenshot screenshot; | |
| zx_status_t status = this->GetEnclosedGuest()->GetScenic()->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) { | |
| // Take a screenshot. | |
| Screenshot screenshot1; | |
| zx_status_t status = this->GetEnclosedGuest()->GetScenic()->CaptureScreenshot(&screenshot1); | |
| ASSERT_EQ(status, ZX_OK) << "Error capturing screenshot."; | |
| SaveScreenshot("input-state1", screenshot1); | |
| // Type a key, which should update the display. | |
| this->GetEnclosedGuest()->GetScenic()->SendKeyPress(KeyboardEventHidUsage::KEY_A); | |
| // 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 |